181 lines
6.5 KiB
TypeScript
181 lines
6.5 KiB
TypeScript
import 'reflect-metadata';
|
|
import 'katex/dist/katex.min.css';
|
|
import { StrictMode } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import './styles/variables.css';
|
|
import './index.css';
|
|
import App from './App.tsx';
|
|
import DialogProvider from './components/common/DialogProvider';
|
|
import { KGCore } from './core/KGCore';
|
|
import { KGAudioInterface } from './core/audio-interface/KGAudioInterface';
|
|
import { ConfigManager } from './core/config/ConfigManager';
|
|
import { KGMidiInput } from './core/midi-input/KGMidiInput';
|
|
import { KGDebugger } from './core/KGDebugger';
|
|
import { enumerateAudioDevices, validateConfiguredAudioDevices } from './util/audioDeviceUtil';
|
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
|
|
const root = createRoot(document.getElementById('root')!);
|
|
|
|
// OPFS requires a secure context (HTTPS or localhost). Show a clear error instead of crashing.
|
|
if (!window.isSecureContext) {
|
|
root.render(
|
|
<StrictMode>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100vh',
|
|
gap: '16px',
|
|
padding: '32px',
|
|
textAlign: 'center',
|
|
color: 'rgba(255,255,255,0.87)',
|
|
background: '#1e1e1e',
|
|
fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
|
|
}}
|
|
>
|
|
<div style={{ fontSize: '48px' }}>🔒</div>
|
|
<h1 style={{ fontSize: '22px', fontWeight: 600, margin: 0 }}>Secure Context Required</h1>
|
|
<p style={{ fontSize: '14px', color: 'rgba(255,255,255,0.55)', maxWidth: '480px', lineHeight: 1.6 }}>
|
|
K.G.Studio uses the browser's Origin Private File System (OPFS) to store projects,
|
|
which requires a <strong style={{ color: 'rgba(255,255,255,0.8)' }}>secure context</strong>.
|
|
</p>
|
|
<div
|
|
style={{
|
|
background: '#2a2a2a',
|
|
border: '1px solid #444',
|
|
borderRadius: '8px',
|
|
padding: '16px 24px',
|
|
fontSize: '13px',
|
|
lineHeight: 2,
|
|
color: 'rgba(255,255,255,0.7)',
|
|
}}
|
|
>
|
|
<div>Use <code style={{ color: '#7cb8ff' }}>https://</code> instead of <code style={{ color: '#f28b82' }}>http://</code></div>
|
|
<div>or access via <code style={{ color: '#7cb8ff' }}>localhost</code> / <code style={{ color: '#7cb8ff' }}>127.0.0.1</code></div>
|
|
</div>
|
|
</div>
|
|
</StrictMode>,
|
|
);
|
|
} else {
|
|
// Initialize KGCore instance
|
|
await KGCore.instance().initialize();
|
|
|
|
const configManager = ConfigManager.instance();
|
|
try {
|
|
const deviceSnapshot = await enumerateAudioDevices();
|
|
const validatedDevices = validateConfiguredAudioDevices(
|
|
configManager.get('audio.input_device_id') as string | undefined,
|
|
configManager.get('audio.output_device_id') as string | undefined,
|
|
deviceSnapshot
|
|
);
|
|
|
|
const updates: Array<Promise<void>> = [];
|
|
const statusMessages: string[] = [];
|
|
|
|
if (validatedDevices.inputFellBackToDefault) {
|
|
updates.push(configManager.set('audio.input_device_id', 'default'));
|
|
statusMessages.push('Previously selected audio input device is unavailable; using System Default.');
|
|
}
|
|
if (validatedDevices.outputFellBackToDefault) {
|
|
updates.push(configManager.set('audio.output_device_id', 'default'));
|
|
statusMessages.push('Previously selected audio output device is unavailable; using System Default.');
|
|
}
|
|
|
|
if (updates.length > 0) {
|
|
await Promise.all(updates);
|
|
KGCore.instance().setStatus(statusMessages.join(' '));
|
|
}
|
|
|
|
await KGAudioInterface.instance().applyConfiguredOutputDevice(validatedDevices.outputDeviceId);
|
|
} catch (error) {
|
|
console.warn('Audio device validation skipped:', error);
|
|
}
|
|
|
|
// Initialize KGMidiInput instance
|
|
await KGMidiInput.instance().initialize();
|
|
|
|
// Attach debugger to global window in development mode
|
|
if (import.meta.env.DEV) {
|
|
const dbg = KGDebugger.instance();
|
|
(window as unknown as { KGDebugger: KGDebugger; sh: () => Promise<void> }).KGDebugger = dbg;
|
|
(window as unknown as { sh: () => Promise<void> }).sh = () => dbg.startShell();
|
|
console.log('🔧 KGDebugger attached to window - try: KGDebugger.help() or sh()');
|
|
}
|
|
|
|
// Start audio context on first user interaction
|
|
let audioContextStarted = false;
|
|
const tryStartAudioContext = async () => {
|
|
if (!audioContextStarted) {
|
|
audioContextStarted = true;
|
|
try {
|
|
const audioInterface = KGAudioInterface.instance();
|
|
if (!audioInterface.getIsAudioContextStarted()) {
|
|
await audioInterface.startAudioContext();
|
|
console.log('Audio context started on first user interaction');
|
|
}
|
|
} catch (error) {
|
|
console.log('Audio context start failed:', error);
|
|
audioContextStarted = false; // Allow retry
|
|
}
|
|
}
|
|
};
|
|
|
|
// Request MIDI access on first user interaction
|
|
let midiAccessRequested = false;
|
|
const tryRequestMIDIAccess = async () => {
|
|
if (!midiAccessRequested) {
|
|
midiAccessRequested = true;
|
|
try {
|
|
const midiInput = KGMidiInput.instance();
|
|
if (!midiInput.getMIDIAccess()) {
|
|
await midiInput.requestMIDIAccess();
|
|
console.log('MIDI access granted on first user interaction');
|
|
}
|
|
} catch (error) {
|
|
console.log('MIDI access failed:', error);
|
|
midiAccessRequested = false; // Allow retry
|
|
}
|
|
}
|
|
};
|
|
|
|
// Combined handler for first user interaction
|
|
const handleFirstInteraction = async () => {
|
|
await tryStartAudioContext();
|
|
await tryRequestMIDIAccess();
|
|
|
|
// Remove listeners after first attempt
|
|
document.removeEventListener('click', handleFirstInteraction);
|
|
document.removeEventListener('touchstart', handleFirstInteraction);
|
|
document.removeEventListener('keydown', handleFirstInteraction);
|
|
};
|
|
|
|
// Listen for first user interaction
|
|
document.addEventListener('click', handleFirstInteraction, { passive: true });
|
|
document.addEventListener('touchstart', handleFirstInteraction, { passive: true });
|
|
document.addEventListener('keydown', handleFirstInteraction, { passive: true });
|
|
|
|
// Add event listener for beforeunload event
|
|
window.addEventListener('beforeunload', (event) => {
|
|
// Cancel the event
|
|
event.preventDefault();
|
|
// Chrome requires returnValue to be set
|
|
event.returnValue = '';
|
|
|
|
// The browser will show a standard confirmation dialog
|
|
// with a message like "Changes you made may not be saved."
|
|
// Note: Custom messages are no longer supported in modern browsers for security reasons
|
|
});
|
|
|
|
root.render(
|
|
<StrictMode>
|
|
<I18nProvider>
|
|
<DialogProvider>
|
|
<App />
|
|
</DialogProvider>
|
|
</I18nProvider>
|
|
</StrictMode>,
|
|
);
|
|
} // end isSecureContext else
|