Spaces:
Running
Running
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import '@tailwindcss/browser'; | |
| //Gemini 95 was fully vibe-coded by @ammaar and @olacombe, while we don't endorse code quality, we thought it was a fun demonstration of what's possible with the model when a Designer and PM jam. | |
| //An homage to an OS that inspired so many of us! | |
| // Define the dosInstances object to fix type errors | |
| const dosInstances: Record<string, { initialized: boolean }> = {}; | |
| // --- DOM Element References --- | |
| const desktop = document.getElementById('desktop') as HTMLDivElement; | |
| const windows = document.querySelectorAll('.window') as NodeListOf<HTMLDivElement>; | |
| const icons = document.querySelectorAll('.icon') as NodeListOf<HTMLDivElement>; // This is a NodeList | |
| const startMenu = document.getElementById('start-menu') as HTMLDivElement; | |
| const startButton = document.getElementById('start-button') as HTMLButtonElement; | |
| const taskbarAppsContainer = document.getElementById('taskbar-apps') as HTMLDivElement; | |
| const paintAssistant = document.getElementById('paint-assistant') as HTMLDivElement; | |
| const assistantBubble = paintAssistant?.querySelector('.assistant-bubble') as HTMLDivElement; | |
| // --- State Variables --- | |
| let activeWindow: HTMLDivElement | null = null; | |
| let highestZIndex: number = 20; // Start z-index for active windows | |
| const openApps = new Map<string, { windowEl: HTMLDivElement; taskbarButton: HTMLDivElement }>(); // Store open apps and their elements | |
| let geminiInstance: any | null = null; // Store the initialized Gemini AI instance | |
| let paintCritiqueIntervalId: number | null = null; // Timer for paint critiques | |
| // Store ResizeObservers to disconnect them later | |
| const paintResizeObserverMap = new Map<Element, ResizeObserver>(); | |
| // --- Minesweeper Game State Variables --- | |
| let minesweeperTimerInterval: number | null = null; | |
| let minesweeperTimeElapsed: number = 0; | |
| let minesweeperFlagsPlaced: number = 0; | |
| let minesweeperGameOver: boolean = false; | |
| let minesweeperMineCount: number = 10; // Default for 9x9 | |
| let minesweeperGridSize: { rows: number, cols: number } = { rows: 9, cols: 9 }; // Default 9x9 | |
| let minesweeperFirstClick: boolean = true; // To ensure first click is never a mine | |
| // --- YouTube Player State --- | |
| // @ts-ignore: YT will be defined by the YouTube API script | |
| const youtubePlayers: Record<string, YT.Player | null> = {}; | |
| let ytApiLoaded = false; | |
| let ytApiLoadingPromise: Promise<void> | null = null; | |
| const DEFAULT_YOUTUBE_VIDEO_ID = 'WXuK6gekU1Y'; // Default video for GemPlayer ("Never Gonna Give You Up") | |
| // --- Core Functions --- | |
| /** Brings a window to the front and sets it as active */ | |
| function bringToFront(windowElement: HTMLDivElement): void { | |
| if (activeWindow === windowElement) return; // Already active | |
| if (activeWindow) { | |
| activeWindow.classList.remove('active'); | |
| const appName = activeWindow.id; | |
| if (openApps.has(appName)) { | |
| openApps.get(appName)?.taskbarButton.classList.remove('active'); | |
| } | |
| } | |
| highestZIndex++; | |
| windowElement.style.zIndex = highestZIndex.toString(); | |
| windowElement.classList.add('active'); | |
| activeWindow = windowElement; | |
| const appNameRef = windowElement.id; | |
| if (openApps.has(appNameRef)) { | |
| openApps.get(appNameRef)?.taskbarButton.classList.add('active'); | |
| } | |
| if ((appNameRef === 'doom' || appNameRef === 'wolf3d') && dosInstances[appNameRef]) { | |
| const container = document.getElementById(`${appNameRef}-container`); // This ID might need checking | |
| const canvas = container?.querySelector('canvas'); | |
| canvas?.focus(); | |
| } | |
| } | |
| /** Opens an application window */ | |
| async function openApp(appName: string): Promise<void> { | |
| const windowElement = document.getElementById(appName) as HTMLDivElement | null; | |
| if (!windowElement) { | |
| console.error(`Window element not found for app: ${appName}`); | |
| return; | |
| } | |
| if (openApps.has(appName)) { | |
| bringToFront(windowElement); | |
| windowElement.style.display = 'flex'; | |
| windowElement.classList.add('active'); | |
| return; | |
| } | |
| windowElement.style.display = 'flex'; | |
| windowElement.classList.add('active'); | |
| bringToFront(windowElement); | |
| const taskbarButton = document.createElement('div'); | |
| taskbarButton.classList.add('taskbar-app'); | |
| taskbarButton.dataset.appName = appName; | |
| let iconSrc = ''; | |
| let title = appName; | |
| const iconElement = findIconElement(appName); | |
| if (iconElement) { | |
| const img = iconElement.querySelector('img'); | |
| const span = iconElement.querySelector('span'); | |
| if(img) iconSrc = img.src; | |
| if(span) title = span.textContent || appName; | |
| } else { // Fallback for apps opened via start menu but maybe no desktop icon | |
| switch(appName) { | |
| case 'myComputer': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/mycomputer.png'; title = 'My Gemtop'; break; | |
| case 'chrome': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/chrome-icon-2.png'; title = 'Chrome'; break; | |
| case 'notepad': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/GemNotes.png'; title = 'GemNotes'; break; | |
| case 'paint': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/gempaint.png'; title = 'GemPaint'; break; | |
| case 'doom': iconSrc = 'https://64.media.tumblr.com/1d89dfa76381e5c14210a2149c83790d/7a15f84c681c1cf9-c1/s540x810/86985984be99d5591e0cbc0dea6f05ffa3136dac.png'; title = 'Doom II'; break; | |
| case 'gemini': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/GeminiChatRetro.png'; title = 'Gemini App'; break; | |
| case 'minesweeper': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/gemsweeper.png'; title = 'GemSweeper'; break; | |
| case 'imageViewer': iconSrc = 'https://win98icons.alexmeub.com/icons/png/display_properties-4.png'; title = 'Image Viewer'; break; | |
| case 'mediaPlayer': iconSrc = 'https://storage.googleapis.com/gemini-95-icons/ytmediaplayer.png'; title = 'GemPlayer'; break; | |
| } | |
| } | |
| if (iconSrc) { | |
| const img = document.createElement('img'); | |
| img.src = iconSrc; | |
| img.alt = title; | |
| taskbarButton.appendChild(img); | |
| } | |
| taskbarButton.appendChild(document.createTextNode(title)); | |
| taskbarButton.addEventListener('click', () => { | |
| if (windowElement === activeWindow && windowElement.style.display !== 'none') { | |
| minimizeApp(appName); | |
| } else { | |
| windowElement.style.display = 'flex'; | |
| bringToFront(windowElement); | |
| } | |
| }); | |
| taskbarAppsContainer.appendChild(taskbarButton); | |
| openApps.set(appName, { windowEl: windowElement, taskbarButton: taskbarButton }); | |
| taskbarButton.classList.add('active'); | |
| // Initialize specific applications | |
| if (appName === 'chrome') { | |
| initAiBrowser(windowElement); | |
| } | |
| else if (appName === 'notepad') { | |
| await initNotepadStory(windowElement); | |
| } | |
| else if (appName === 'paint') { | |
| initSimplePaintApp(windowElement); | |
| if (paintAssistant) paintAssistant.classList.add('visible'); | |
| if (assistantBubble) assistantBubble.textContent = 'Warming up my judging circuits...'; | |
| if (paintCritiqueIntervalId) clearInterval(paintCritiqueIntervalId); | |
| paintCritiqueIntervalId = window.setInterval(critiquePaintDrawing, 15000); | |
| } | |
| else if (appName === 'doom' && !dosInstances['doom']) { | |
| const doomContainer = document.getElementById('doom-content') as HTMLDivElement; | |
| if (doomContainer) { | |
| doomContainer.innerHTML = '<iframe src="https://js-dos.com/games/doom.exe.html" width="100%" height="100%" frameborder="0" scrolling="no" allowfullscreen></iframe>'; | |
| dosInstances['doom'] = { initialized: true }; | |
| } | |
| } else if (appName === 'gemini') { | |
| await initGeminiChat(windowElement); | |
| } | |
| else if (appName === 'minesweeper') { | |
| initMinesweeperGame(windowElement); | |
| } | |
| else if (appName === 'myComputer') { | |
| initMyComputer(windowElement); | |
| } | |
| else if (appName === 'mediaPlayer') { | |
| await initMediaPlayer(windowElement); | |
| } | |
| } | |
| /** Closes an application window */ | |
| function closeApp(appName: string): void { | |
| const appData = openApps.get(appName); | |
| if (!appData) return; | |
| const { windowEl, taskbarButton } = appData; | |
| windowEl.style.display = 'none'; | |
| windowEl.classList.remove('active'); | |
| taskbarButton.remove(); | |
| openApps.delete(appName); | |
| if (dosInstances[appName]) { | |
| console.log(`Cleaning up ${appName} instance (iframe approach)`); | |
| const container = document.getElementById(`${appName}-content`); | |
| if (container) container.innerHTML = ''; | |
| delete dosInstances[appName]; | |
| } | |
| if (appName === 'paint') { | |
| if (paintCritiqueIntervalId) { | |
| clearInterval(paintCritiqueIntervalId); | |
| paintCritiqueIntervalId = null; | |
| if (paintAssistant) paintAssistant.classList.remove('visible'); | |
| } | |
| const paintContent = appData.windowEl.querySelector('.window-content') as HTMLDivElement | null; | |
| if (paintContent && paintResizeObserverMap.has(paintContent)) { | |
| paintResizeObserverMap.get(paintContent)?.disconnect(); | |
| paintResizeObserverMap.delete(paintContent); | |
| } | |
| } | |
| if (appName === 'minesweeper') { | |
| if (minesweeperTimerInterval) { | |
| clearInterval(minesweeperTimerInterval); | |
| minesweeperTimerInterval = null; | |
| } | |
| } | |
| if (appName === 'mediaPlayer') { | |
| const player = youtubePlayers[appName]; | |
| if (player) { | |
| try { | |
| if (typeof player.stopVideo === 'function') player.stopVideo(); | |
| if (typeof player.destroy === 'function') player.destroy(); | |
| } catch (e) { | |
| console.warn("Error stopping/destroying media player:", e); | |
| } | |
| delete youtubePlayers[appName]; | |
| console.log("Destroyed YouTube player for mediaPlayer."); | |
| } | |
| // Reset the player area with a message | |
| const playerDivId = `youtube-player-${appName}`; | |
| const playerDiv = document.getElementById(playerDivId) as HTMLDivElement | null; | |
| if (playerDiv) { | |
| playerDiv.innerHTML = `<p class="media-player-status-message">Player closed. Enter a YouTube URL to load.</p>`; | |
| } | |
| // Reset control buttons state (optional, but good practice) | |
| const mediaPlayerWindow = document.getElementById('mediaPlayer'); | |
| if (mediaPlayerWindow) { | |
| const playBtn = mediaPlayerWindow.querySelector('#media-player-play') as HTMLButtonElement; | |
| const pauseBtn = mediaPlayerWindow.querySelector('#media-player-pause') as HTMLButtonElement; | |
| const stopBtn = mediaPlayerWindow.querySelector('#media-player-stop') as HTMLButtonElement; | |
| if (playBtn) playBtn.disabled = true; | |
| if (pauseBtn) pauseBtn.disabled = true; | |
| if (stopBtn) stopBtn.disabled = true; | |
| } | |
| } | |
| if (activeWindow === windowEl) { | |
| activeWindow = null; | |
| let nextAppToActivate: HTMLDivElement | null = null; | |
| let maxZ = -1; | |
| openApps.forEach((data) => { | |
| const z = parseInt(data.windowEl.style.zIndex || '0', 10); | |
| if (z > maxZ) { | |
| maxZ = z; | |
| nextAppToActivate = data.windowEl; | |
| } | |
| }); | |
| if (nextAppToActivate) { | |
| bringToFront(nextAppToActivate); | |
| } | |
| } | |
| } | |
| /** Minimizes an application window */ | |
| function minimizeApp(appName: string): void { | |
| const appData = openApps.get(appName); | |
| if (!appData) return; | |
| const { windowEl, taskbarButton } = appData; | |
| windowEl.style.display = 'none'; | |
| windowEl.classList.remove('active'); | |
| taskbarButton.classList.remove('active'); | |
| if (activeWindow === windowEl) { | |
| activeWindow = null; | |
| let nextAppToActivate: string | null = null; | |
| let maxZ = 0; | |
| openApps.forEach((data, name) => { | |
| if (data.windowEl.style.display !== 'none') { | |
| const z = parseInt(data.windowEl.style.zIndex || '0', 10); | |
| if (z > maxZ) { | |
| maxZ = z; | |
| nextAppToActivate = name; | |
| } | |
| } | |
| }); | |
| if (nextAppToActivate) { | |
| bringToFront(openApps.get(nextAppToActivate)!.windowEl); | |
| } | |
| } | |
| } | |
| // --- Gemini Chat Specific Functions --- | |
| async function initGeminiChat(windowElement: HTMLDivElement): Promise<void> { | |
| const historyDiv = windowElement.querySelector('.gemini-chat-history') as HTMLDivElement; | |
| const inputEl = windowElement.querySelector('.gemini-chat-input') as HTMLInputElement; | |
| const sendButton = windowElement.querySelector('.gemini-chat-send') as HTMLButtonElement; | |
| if (!historyDiv || !inputEl || !sendButton) { | |
| console.error("Gemini chat elements not found in window:", windowElement.id); | |
| return; | |
| } | |
| function addChatMessage(container: HTMLDivElement, text: string, className: string = '') { | |
| const p = document.createElement('p'); | |
| if (className) p.classList.add(className); | |
| p.textContent = text; | |
| container.appendChild(p); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| addChatMessage(historyDiv, "Initializing AI...", "system-message"); | |
| const sendMessage = async () => { | |
| if (!geminiInstance) { | |
| const initSuccess = await initializeGeminiIfNeeded('initGeminiChat'); | |
| if (!initSuccess) { | |
| addChatMessage(historyDiv, "Error: Failed to initialize AI.", "error-message"); | |
| return; | |
| } | |
| const initMsg = Array.from(historyDiv.children).find(el => el.textContent?.includes("Initializing AI...")); | |
| if (initMsg) initMsg.remove(); | |
| addChatMessage(historyDiv, "AI Ready.", "system-message"); | |
| } | |
| const message = inputEl.value.trim(); | |
| if (!message) return; | |
| addChatMessage(historyDiv, `You: ${message}`, "user-message"); | |
| inputEl.value = ''; | |
| inputEl.disabled = true; | |
| sendButton.disabled = true; | |
| try { | |
| // @ts-ignore | |
| const chat = geminiInstance.chats.create({ model: 'gemini-2.5-flash', history: [] }); | |
| // @ts-ignore | |
| const result = await chat.sendMessageStream({message: message}); | |
| let fullResponse = ""; | |
| addChatMessage(historyDiv, "Gemini: ", "gemini-message"); | |
| const lastMessageElement = historyDiv.lastElementChild as HTMLParagraphElement | null; | |
| for await (const chunk of result) { | |
| const chunkText = chunk.text || ""; | |
| fullResponse += chunkText; | |
| if (lastMessageElement) { | |
| lastMessageElement.textContent += chunkText; | |
| historyDiv.scrollTop = historyDiv.scrollHeight; | |
| } | |
| } | |
| } catch (error: any) { | |
| addChatMessage(historyDiv, `Error: ${error.message || 'Failed to get response.'}`, "error-message"); | |
| } finally { | |
| inputEl.disabled = false; sendButton.disabled = false; inputEl.focus(); | |
| } | |
| }; | |
| sendButton.onclick = sendMessage; | |
| inputEl.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; | |
| inputEl.disabled = false; sendButton.disabled = false; inputEl.focus(); | |
| } | |
| /** Handles Notepad story generation */ | |
| async function initNotepadStory(windowElement: HTMLDivElement): Promise<void> { | |
| const textarea = windowElement.querySelector('.notepad-textarea') as HTMLTextAreaElement; | |
| const storyButton = windowElement.querySelector('.notepad-story-button') as HTMLButtonElement; | |
| if (!textarea || !storyButton) return; | |
| storyButton.addEventListener('click', async () => { | |
| const currentText = textarea.value; | |
| textarea.value = currentText + "\n\nGenerating story... Please wait...\n\n"; | |
| textarea.scrollTop = textarea.scrollHeight; | |
| storyButton.disabled = true; storyButton.textContent = "Working..."; | |
| try { | |
| if (!geminiInstance) { | |
| if (!await initializeGeminiIfNeeded('initNotepadStory')) throw new Error("Failed to initialize Gemini API."); | |
| } | |
| const prompt = "Write me a short creative story (250-300 words) with an unexpected twist ending. Make it engaging and suitable for all ages."; | |
| // @ts-ignore | |
| const result = await geminiInstance.models.generateContentStream({ model: 'gemini-2.5-flash', contents: prompt }); | |
| textarea.value = currentText + "\n\n"; | |
| for await (const chunk of result) { | |
| textarea.value += chunk.text || ""; | |
| textarea.scrollTop = textarea.scrollHeight; | |
| } | |
| textarea.value += "\n\n"; | |
| } catch (error: any) { | |
| textarea.value = currentText + "\n\nError: " + (error.message || "Failed to generate story.") + "\n\n"; | |
| } finally { | |
| storyButton.disabled = false; storyButton.textContent = "Generate Story"; | |
| textarea.scrollTop = textarea.scrollHeight; | |
| } | |
| }); | |
| } | |
| /** Initializes the AI Browser functionality with image generation */ | |
| function initAiBrowser(windowElement: HTMLDivElement): void { | |
| const addressBar = windowElement.querySelector('.browser-address-bar') as HTMLInputElement; | |
| const goButton = windowElement.querySelector('.browser-go-button') as HTMLButtonElement; | |
| const iframe = windowElement.querySelector('#browser-frame') as HTMLIFrameElement; | |
| const loadingEl = windowElement.querySelector('.browser-loading') as HTMLDivElement; | |
| const DIAL_UP_SOUND_URL = 'https://www.soundjay.com/communication/dial-up-modem-01.mp3'; | |
| let dialUpAudio: HTMLAudioElement | null = null; | |
| if (!addressBar || !goButton || !iframe || !loadingEl) return; | |
| async function navigateToUrl(url: string): Promise<void> { | |
| if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url; | |
| try { | |
| const urlObj = new URL(url); | |
| const domain = urlObj.hostname; | |
| // --- RESTORED DIALUP ANIMATION --- | |
| loadingEl.innerHTML = ` | |
| <style> | |
| .dialup-animation .dot { | |
| animation: dialup-blink 1.4s infinite both; | |
| } | |
| .dialup-animation .dot:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .dialup-animation .dot:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes dialup-blink { | |
| 0%, 80%, 100% { opacity: 0; } | |
| 40% { opacity: 1; } | |
| } | |
| .browser-loading p { margin: 5px 0; } | |
| .browser-loading .small-text { font-size: 0.8em; color: #aaa; } | |
| </style> | |
| <img src="https://d112y698adiu2z.cloudfront.net/photos/production/software_photos/000/948/341/datas/original.gif"/> | |
| <p>Connecting to ${domain}<span class="dialup-animation"><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span></span></p> | |
| <!-- Sound will play via JS --> | |
| `; | |
| loadingEl.style.display = 'flex'; | |
| // --- END RESTORED DIALUP ANIMATION --- | |
| try { | |
| if (!dialUpAudio) { | |
| dialUpAudio = new Audio(DIAL_UP_SOUND_URL); dialUpAudio.loop = true; | |
| } | |
| await dialUpAudio.play(); | |
| } catch (audioError) { console.error("Dial-up sound error:", audioError); } | |
| try { | |
| if (!geminiInstance) { | |
| if (!await initializeGeminiIfNeeded('initAiBrowser')) { | |
| iframe.src = 'data:text/plain;charset=utf-8,AI Init Error'; | |
| loadingEl.style.display = 'none'; return; | |
| } | |
| } | |
| const websitePrompt = ` | |
| Create a complete 90s-style website for the domain "${domain}". | |
| MUST include: 1 relevant image, garish 90s styling (neon, comic sans, tables), content specific to "${domain}", scrolling marquee, retro emoji/ascii, blinking text, visitor counter (9000+), "Under Construction" signs. Fun, humorous, 1996 feel. Image MUST match theme. No modern design. | |
| `; | |
| // @ts-ignore | |
| const result = await geminiInstance.models.generateContent({ | |
| model: 'gemini-2.0-flash-preview-image-generation', | |
| contents: [{role: 'user', parts: [{text: websitePrompt}]}], | |
| config: { temperature: 0.9, responseModalities: ['TEXT', 'IMAGE'] } | |
| }); | |
| let htmlContent = ""; const images: string[] = []; | |
| if (result.candidates?.[0]?.content) { | |
| for (const part of result.candidates[0].content.parts) { | |
| if (part.text) htmlContent += part.text.replace(/```html|```/g, '').trim(); | |
| else if (part.inlineData?.data) images.push(`data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`); | |
| } | |
| } | |
| if (!htmlContent.includes("<html")) { | |
| htmlContent = `<!DOCTYPE html><html><head><title>${domain}</title><style>body{font-family:"Comic Sans MS";background:lime;color:blue;}marquee{background:yellow;color:red;}img{max-width:80%; display:block; margin:10px auto; border: 3px ridge gray;}</style></head><body><marquee>Welcome to ${domain}!</marquee><h1>${domain}</h1><div>${htmlContent}</div></body></html>`; | |
| } | |
| if (images.length > 0) { | |
| if (!htmlContent.includes('<img src="data:')) { | |
| htmlContent = htmlContent.replace(/(<\/h1>)/i, `$1\n<img src="${images[0]}" alt="Site Image">`); | |
| } | |
| } | |
| iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(htmlContent); | |
| addressBar.value = url; | |
| } catch (e: any) { | |
| iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(`<html><body>Error generating site: ${e.message}</body></html>`); | |
| } finally { | |
| loadingEl.style.display = 'none'; | |
| if (dialUpAudio) { dialUpAudio.pause(); dialUpAudio.currentTime = 0; } | |
| } | |
| } catch (e) { alert("Invalid URL"); loadingEl.style.display = 'none'; } | |
| } | |
| goButton.addEventListener('click', () => navigateToUrl(addressBar.value)); | |
| addressBar.addEventListener('keydown', (e) => { if (e.key === 'Enter') navigateToUrl(addressBar.value); }); | |
| addressBar.addEventListener('click', () => addressBar.select()); | |
| } | |
| // --- Event Listeners Setup --- | |
| icons.forEach(icon => { | |
| icon.addEventListener('click', () => { | |
| const appName = icon.getAttribute('data-app'); | |
| if (appName) { | |
| openApp(appName); | |
| startMenu.classList.remove('active'); | |
| } | |
| }); | |
| }); | |
| document.querySelectorAll('.start-menu-item').forEach(item => { | |
| item.addEventListener('click', () => { | |
| const appName = (item as HTMLElement).getAttribute('data-app'); | |
| if (appName) openApp(appName); | |
| startMenu.classList.remove('active'); | |
| }); | |
| }); | |
| startButton.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| startMenu.classList.toggle('active'); | |
| if (startMenu.classList.contains('active')) { | |
| highestZIndex++; | |
| startMenu.style.zIndex = highestZIndex.toString(); | |
| } | |
| }); | |
| windows.forEach(windowElement => { | |
| const titleBar = windowElement.querySelector('.window-titlebar') as HTMLDivElement | null; | |
| const closeButton = windowElement.querySelector('.window-close') as HTMLDivElement | null; | |
| const minimizeButton = windowElement.querySelector('.window-minimize') as HTMLDivElement | null; | |
| windowElement.addEventListener('mousedown', () => bringToFront(windowElement), true); | |
| if (closeButton) { | |
| closeButton.addEventListener('click', (e) => { e.stopPropagation(); closeApp(windowElement.id); }); | |
| } | |
| if (minimizeButton) { | |
| minimizeButton.addEventListener('click', (e) => { e.stopPropagation(); minimizeApp(windowElement.id); }); | |
| } | |
| if (titleBar) { | |
| let isDragging = false; | |
| let dragOffsetX: number, dragOffsetY: number; | |
| const startDragging = (e: MouseEvent) => { | |
| if (!(e.target === titleBar || titleBar.contains(e.target as Node)) || (e.target as Element).closest('.window-control-button')) { | |
| isDragging = false; return; | |
| } | |
| isDragging = true; bringToFront(windowElement); | |
| const rect = windowElement.getBoundingClientRect(); | |
| dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; | |
| titleBar.style.cursor = 'grabbing'; | |
| document.addEventListener('mousemove', dragWindow); | |
| document.addEventListener('mouseup', stopDragging, { once: true }); | |
| }; | |
| const dragWindow = (e: MouseEvent) => { | |
| if (!isDragging) return; | |
| let x = e.clientX - dragOffsetX; let y = e.clientY - dragOffsetY; | |
| const taskbarHeight = taskbarAppsContainer.parentElement?.offsetHeight ?? 36; | |
| const maxX = window.innerWidth - windowElement.offsetWidth; | |
| const maxY = window.innerHeight - windowElement.offsetHeight - taskbarHeight; | |
| const minX = -(windowElement.offsetWidth - 40); | |
| const maxXAdjusted = window.innerWidth - 40; | |
| x = Math.max(minX, Math.min(x, maxXAdjusted)); | |
| y = Math.max(0, Math.min(y, maxY)); | |
| windowElement.style.left = `${x}px`; windowElement.style.top = `${y}px`; | |
| }; | |
| const stopDragging = () => { | |
| if (!isDragging) return; | |
| isDragging = false; titleBar.style.cursor = 'grab'; | |
| document.removeEventListener('mousemove', dragWindow); | |
| }; | |
| titleBar.addEventListener('mousedown', startDragging); | |
| } | |
| if (!openApps.has(windowElement.id)) { // Only apply random for newly opened, not for bringToFront | |
| const randomTop = Math.random() * (window.innerHeight / 4) + 20; | |
| const randomLeft = Math.random() * (window.innerWidth / 3) + 20; | |
| windowElement.style.top = `${randomTop}px`; | |
| windowElement.style.left = `${randomLeft}px`; | |
| } | |
| }); | |
| document.addEventListener('click', (e) => { | |
| if (startMenu.classList.contains('active') && !startMenu.contains(e.target as Node) && !startButton.contains(e.target as Node)) { | |
| startMenu.classList.remove('active'); | |
| } | |
| }); | |
| function findIconElement(appName: string): HTMLDivElement | undefined { | |
| return Array.from(icons).find(icon => icon.dataset.app === appName); | |
| } | |
| console.log("Gemini 95 Simulator Initialized (TS)"); | |
| async function critiquePaintDrawing(): Promise<void> { | |
| const paintWindow = document.getElementById('paint') as HTMLDivElement | null; | |
| if (!paintWindow || paintWindow.style.display === 'none') return; | |
| const canvas = paintWindow.querySelector('#paint-canvas') as HTMLCanvasElement | null; | |
| if (!canvas) { if (assistantBubble) assistantBubble.textContent = 'Error: Canvas not found!'; return; } | |
| if (!geminiInstance) { | |
| if (!await initializeGeminiIfNeeded('critiquePaintDrawing')) { | |
| if (assistantBubble) assistantBubble.textContent = 'Error: AI init failed!'; return; | |
| } | |
| } | |
| try { | |
| if (assistantBubble) assistantBubble.textContent = 'Analyzing...'; | |
| const imageDataUrl = canvas.toDataURL('image/jpeg', 0.8); | |
| const base64Data = imageDataUrl.split(',')[1]; | |
| if (!base64Data) throw new Error("Failed to get base64 data."); | |
| const prompt = "Critique this drawing with witty sarcasm (1-2 sentences)."; | |
| const imagePart = { inlineData: { data: base64Data, mimeType: "image/jpeg" } }; | |
| // @ts-ignore | |
| const result = await geminiInstance.models.generateContent({ model: "gemini-2.5-pro-exp-03-25", contents: [{ role: "user", parts: [ { text: prompt }, imagePart] }] }); | |
| const critique = result?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "Is this art?"; | |
| if (assistantBubble) assistantBubble.textContent = critique; | |
| } catch (error: any) { | |
| if (assistantBubble) assistantBubble.textContent = `Critique Error: ${error.message}`; | |
| } | |
| } | |
| function initSimplePaintApp(windowElement: HTMLDivElement): void { | |
| const canvas = windowElement.querySelector('#paint-canvas') as HTMLCanvasElement; | |
| const toolbar = windowElement.querySelector('.paint-toolbar') as HTMLDivElement; | |
| const contentArea = windowElement.querySelector('.window-content') as HTMLDivElement; // This is the direct parent managing canvas size | |
| const colorSwatches = windowElement.querySelectorAll('.paint-color-swatch') as NodeListOf<HTMLButtonElement>; | |
| const sizeButtons = windowElement.querySelectorAll('.paint-size-button') as NodeListOf<HTMLButtonElement>; | |
| const clearButton = windowElement.querySelector('.paint-clear-button') as HTMLButtonElement; | |
| if (!canvas || !toolbar || !contentArea || !clearButton) { return; } | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) { return; } | |
| let isDrawing = false; let lastX = 0; let lastY = 0; | |
| ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; | |
| let currentStrokeStyle = ctx.strokeStyle; let currentLineWidth = ctx.lineWidth; | |
| function resizeCanvas() { | |
| const rect = contentArea.getBoundingClientRect(); | |
| const toolbarHeight = toolbar.offsetHeight; | |
| const newWidth = Math.floor(rect.width); // Canvas width is content area width | |
| const newHeight = Math.floor(rect.height - toolbarHeight); // Canvas height is content area height minus toolbar | |
| if (canvas.width === newWidth && canvas.height === newHeight && newWidth > 0 && newHeight > 0) return; | |
| canvas.width = newWidth > 0 ? newWidth : 1; | |
| canvas.height = newHeight > 0 ? newHeight : 1; | |
| ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| ctx.strokeStyle = currentStrokeStyle; ctx.lineWidth = currentLineWidth; | |
| ctx.lineJoin = 'round'; ctx.lineCap = 'round'; | |
| } | |
| const resizeObserver = new ResizeObserver(resizeCanvas); | |
| resizeObserver.observe(contentArea); | |
| paintResizeObserverMap.set(contentArea, resizeObserver); | |
| resizeCanvas(); | |
| function getMousePos(canvasDom: HTMLCanvasElement, event: MouseEvent | TouchEvent): { x: number, y: number } { | |
| const rect = canvasDom.getBoundingClientRect(); | |
| let clientX, clientY; | |
| if (event instanceof MouseEvent) { clientX = event.clientX; clientY = event.clientY; } | |
| else { clientX = event.touches[0].clientX; clientY = event.touches[0].clientY; } | |
| return { x: clientX - rect.left, y: clientY - rect.top }; | |
| } | |
| function startDrawing(e: MouseEvent | TouchEvent) { | |
| isDrawing = true; const pos = getMousePos(canvas, e); | |
| [lastX, lastY] = [pos.x, pos.y]; ctx.beginPath(); ctx.moveTo(lastX, lastY); | |
| } | |
| function draw(e: MouseEvent | TouchEvent) { | |
| if (!isDrawing) return; e.preventDefault(); | |
| const pos = getMousePos(canvas, e); | |
| ctx.lineTo(pos.x, pos.y); ctx.stroke(); | |
| [lastX, lastY] = [pos.x, pos.y]; | |
| } | |
| function stopDrawing() { if (isDrawing) isDrawing = false; } | |
| canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); | |
| canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseleave', stopDrawing); | |
| canvas.addEventListener('touchstart', startDrawing, { passive: false }); | |
| canvas.addEventListener('touchmove', draw, { passive: false }); | |
| canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); | |
| colorSwatches.forEach(swatch => { | |
| swatch.addEventListener('click', () => { | |
| ctx.strokeStyle = swatch.dataset.color || 'black'; currentStrokeStyle = ctx.strokeStyle; | |
| colorSwatches.forEach(s => s.classList.remove('active')); swatch.classList.add('active'); | |
| if (swatch.dataset.color === 'white') { | |
| const largeSizeButton = Array.from(sizeButtons).find(b => b.dataset.size === '10'); | |
| if (largeSizeButton) { | |
| ctx.lineWidth = parseInt(largeSizeButton.dataset.size || '10', 10); currentLineWidth = ctx.lineWidth; | |
| sizeButtons.forEach(s => s.classList.remove('active')); largeSizeButton.classList.add('active'); | |
| } | |
| } else { | |
| const activeSizeButton = Array.from(sizeButtons).find(b => b.classList.contains('active')); | |
| if (activeSizeButton) { ctx.lineWidth = parseInt(activeSizeButton.dataset.size || '2', 10); currentLineWidth = ctx.lineWidth; } | |
| } | |
| }); | |
| }); | |
| sizeButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| ctx.lineWidth = parseInt(button.dataset.size || '2', 10); currentLineWidth = ctx.lineWidth; | |
| sizeButtons.forEach(s => s.classList.remove('active')); button.classList.add('active'); | |
| const eraser = Array.from(colorSwatches).find(s => s.dataset.color === 'white'); | |
| if (!eraser?.classList.contains('active')) { | |
| if (!Array.from(colorSwatches).some(s => s.classList.contains('active'))) { | |
| const blackSwatch = Array.from(colorSwatches).find(s => s.dataset.color === 'black'); | |
| blackSwatch?.classList.add('active'); ctx.strokeStyle = 'black'; currentStrokeStyle = ctx.strokeStyle; | |
| } | |
| } | |
| }); | |
| }); | |
| clearButton.addEventListener('click', () => { | |
| ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| }); | |
| (windowElement.querySelector('.paint-color-swatch[data-color="black"]') as HTMLButtonElement)?.classList.add('active'); | |
| (windowElement.querySelector('.paint-size-button[data-size="2"]') as HTMLButtonElement)?.classList.add('active'); | |
| } | |
| type MinesweeperCell = { isMine: boolean; isRevealed: boolean; isFlagged: boolean; adjacentMines: number; element: HTMLDivElement; row: number; col: number; }; | |
| function initMinesweeperGame(windowElement: HTMLDivElement): void { | |
| const boardElement = windowElement.querySelector('#minesweeper-board') as HTMLDivElement; | |
| const flagCountElement = windowElement.querySelector('.minesweeper-flag-count') as HTMLDivElement; | |
| const timerElement = windowElement.querySelector('.minesweeper-timer') as HTMLDivElement; | |
| const resetButton = windowElement.querySelector('.minesweeper-reset-button') as HTMLButtonElement; | |
| const hintButton = windowElement.querySelector('.minesweeper-hint-button') as HTMLButtonElement; | |
| const commentaryElement = windowElement.querySelector('.minesweeper-commentary') as HTMLDivElement; | |
| if (!boardElement || !flagCountElement || !timerElement || !resetButton || !hintButton || !commentaryElement) return; | |
| let grid: MinesweeperCell[][] = []; | |
| function resetGame() { | |
| if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval); | |
| minesweeperTimerInterval = null; minesweeperTimeElapsed = 0; minesweeperFlagsPlaced = 0; | |
| minesweeperGameOver = false; minesweeperFirstClick = true; minesweeperMineCount = 10; | |
| minesweeperGridSize = { rows: 9, cols: 9 }; | |
| timerElement.textContent = `⏱️ 0`; flagCountElement.textContent = `🚩 ${minesweeperMineCount}`; | |
| resetButton.textContent = '🙂'; commentaryElement.textContent = "Let's play! Click a square."; | |
| createGrid(); | |
| } | |
| function createGrid() { | |
| boardElement.innerHTML = ''; grid = []; | |
| boardElement.style.gridTemplateColumns = `repeat(${minesweeperGridSize.cols}, 20px)`; | |
| boardElement.style.gridTemplateRows = `repeat(${minesweeperGridSize.rows}, 20px)`; | |
| for (let r = 0; r < minesweeperGridSize.rows; r++) { | |
| const row: MinesweeperCell[] = []; | |
| for (let c = 0; c < minesweeperGridSize.cols; c++) { | |
| const cellElement = document.createElement('div'); cellElement.classList.add('minesweeper-cell'); | |
| const cellData: MinesweeperCell = { isMine: false, isRevealed: false, isFlagged: false, adjacentMines: 0, element: cellElement, row: r, col: c }; | |
| cellElement.addEventListener('click', () => handleCellClick(cellData)); | |
| cellElement.addEventListener('contextmenu', (e) => { e.preventDefault(); handleCellRightClick(cellData); }); | |
| row.push(cellData); boardElement.appendChild(cellElement); | |
| } | |
| grid.push(row); | |
| } | |
| } | |
| function placeMines(firstClickRow: number, firstClickCol: number) { | |
| let minesPlaced = 0; | |
| while (minesPlaced < minesweeperMineCount) { | |
| const r = Math.floor(Math.random() * minesweeperGridSize.rows); | |
| const c = Math.floor(Math.random() * minesweeperGridSize.cols); | |
| if ((r === firstClickRow && c === firstClickCol) || grid[r][c].isMine) continue; | |
| grid[r][c].isMine = true; minesPlaced++; | |
| } | |
| for (let r = 0; r < minesweeperGridSize.rows; r++) { | |
| for (let c = 0; c < minesweeperGridSize.cols; c++) { | |
| if (!grid[r][c].isMine) grid[r][c].adjacentMines = countAdjacentMines(r, c); | |
| } | |
| } | |
| } | |
| function countAdjacentMines(row: number, col: number): number { | |
| let count = 0; | |
| for (let dr = -1; dr <= 1; dr++) { | |
| for (let dc = -1; dc <= 1; dc++) { | |
| if (dr === 0 && dc === 0) continue; | |
| const nr = row + dr; const nc = col + dc; | |
| if (nr >= 0 && nr < minesweeperGridSize.rows && nc >= 0 && nc < minesweeperGridSize.cols && grid[nr][nc].isMine) count++; | |
| } | |
| } | |
| return count; | |
| } | |
| function handleCellClick(cell: MinesweeperCell) { | |
| if (minesweeperGameOver || cell.isRevealed || cell.isFlagged) return; | |
| if (minesweeperFirstClick && !minesweeperTimerInterval) { | |
| placeMines(cell.row, cell.col); minesweeperFirstClick = false; startTimer(); | |
| } | |
| if (cell.isMine) gameOver(cell); | |
| else { revealCell(cell); checkWinCondition(); } | |
| } | |
| function handleCellRightClick(cell: MinesweeperCell) { | |
| if (minesweeperGameOver || cell.isRevealed || (minesweeperFirstClick && !minesweeperTimerInterval)) return; | |
| cell.isFlagged = !cell.isFlagged; cell.element.textContent = cell.isFlagged ? '🚩' : ''; | |
| minesweeperFlagsPlaced += cell.isFlagged ? 1 : -1; | |
| updateFlagCount(); checkWinCondition(); | |
| } | |
| function revealCell(cell: MinesweeperCell) { | |
| if (cell.isRevealed || cell.isFlagged || cell.isMine) return; | |
| cell.isRevealed = true; cell.element.classList.add('revealed'); cell.element.textContent = ''; | |
| if (cell.adjacentMines > 0) { | |
| cell.element.textContent = cell.adjacentMines.toString(); | |
| cell.element.dataset.number = cell.adjacentMines.toString(); | |
| } else { | |
| for (let dr = -1; dr <= 1; dr++) { | |
| for (let dc = -1; dc <= 1; dc++) { | |
| if (dr === 0 && dc === 0) continue; | |
| const nr = cell.row + dr; const nc = cell.col + dc; | |
| if (nr >= 0 && nr < minesweeperGridSize.rows && nc >= 0 && nc < minesweeperGridSize.cols) { | |
| const neighbor = grid[nr][nc]; | |
| if (!neighbor.isRevealed && !neighbor.isFlagged) revealCell(neighbor); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| function startTimer() { | |
| if (minesweeperTimerInterval) return; | |
| minesweeperTimeElapsed = 0; timerElement.textContent = `⏱️ 0`; | |
| minesweeperTimerInterval = window.setInterval(() => { | |
| minesweeperTimeElapsed++; timerElement.textContent = `⏱️ ${minesweeperTimeElapsed}`; | |
| }, 1000); | |
| } | |
| function updateFlagCount() { | |
| flagCountElement.textContent = `🚩 ${minesweeperMineCount - minesweeperFlagsPlaced}`; | |
| } | |
| function gameOver(clickedMine: MinesweeperCell) { | |
| minesweeperGameOver = true; | |
| if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval); | |
| minesweeperTimerInterval = null; resetButton.textContent = '😵'; | |
| grid.forEach(row => row.forEach(cell => { | |
| if (cell.isMine) { | |
| cell.element.classList.add('mine', 'revealed'); cell.element.textContent = '💣'; | |
| } | |
| if (!cell.isMine && cell.isFlagged) cell.element.textContent = '❌'; | |
| })); | |
| clickedMine.element.classList.add('exploded'); clickedMine.element.textContent = '💥'; | |
| } | |
| function checkWinCondition() { | |
| if (minesweeperGameOver) return; | |
| let revealedCount = 0; let correctlyFlaggedMines = 0; | |
| grid.forEach(row => row.forEach(cell => { | |
| if (cell.isRevealed && !cell.isMine) revealedCount++; | |
| if (cell.isFlagged && cell.isMine) correctlyFlaggedMines++; | |
| })); | |
| const totalNonMineCells = (minesweeperGridSize.rows * minesweeperGridSize.cols) - minesweeperMineCount; | |
| if (revealedCount === totalNonMineCells || (correctlyFlaggedMines === minesweeperMineCount && minesweeperFlagsPlaced === minesweeperMineCount)) { | |
| minesweeperGameOver = true; | |
| if (minesweeperTimerInterval) clearInterval(minesweeperTimerInterval); | |
| minesweeperTimerInterval = null; resetButton.textContent = '😎'; | |
| if (revealedCount === totalNonMineCells) { | |
| grid.forEach(row => row.forEach(cell => { | |
| if (cell.isMine && !cell.isFlagged) { cell.isFlagged = true; cell.element.textContent = '🚩'; minesweeperFlagsPlaced++; } | |
| })); updateFlagCount(); | |
| } | |
| } | |
| } | |
| function getBoardStateAsText(): string { | |
| let boardString = `Flags: ${minesweeperMineCount - minesweeperFlagsPlaced}, Time: ${minesweeperTimeElapsed}s\nGrid (H=Hidden,F=Flag,Num=Mines):\n`; | |
| grid.forEach(row => { | |
| row.forEach(cell => { | |
| if (cell.isFlagged) boardString += " F "; | |
| else if (!cell.isRevealed) boardString += " H "; | |
| else if (cell.adjacentMines > 0) boardString += ` ${cell.adjacentMines} `; | |
| else boardString += " _ "; | |
| }); | |
| boardString += "\n"; | |
| }); | |
| return boardString; | |
| } | |
| async function getAiHint() { | |
| if (minesweeperGameOver || minesweeperFirstClick) { commentaryElement.textContent = "Click a square first!"; return; } | |
| hintButton.disabled = true; hintButton.textContent = '🤔'; commentaryElement.textContent = 'Thinking...'; | |
| if (!geminiInstance) { | |
| if (!await initializeGeminiIfNeeded('getAiHint')) { | |
| commentaryElement.textContent = 'AI Init Error.'; hintButton.disabled = false; hintButton.textContent = '💡 Hint'; return; | |
| } | |
| } | |
| try { | |
| const boardState = getBoardStateAsText(); | |
| const prompt = `Minesweeper state:\n${boardState}\nShort, witty hint (1-2 sentences) for a safe move or dangerous area. Don't reveal exact mines unless certain. Hint:`; | |
| // @ts-ignore | |
| const result = await geminiInstance.models.generateContent({ model: "gemini-2.5-flash", contents: [{role:"user", parts:[{text:prompt}]}], config: {temperature: 0.7}}); | |
| const hintText = result?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "Try clicking somewhere?"; | |
| commentaryElement.textContent = hintText; | |
| } catch (error: any) { commentaryElement.textContent = `Hint Error: ${error.message}`; | |
| } finally { hintButton.disabled = false; hintButton.textContent = '💡 Hint'; } | |
| } | |
| resetButton.addEventListener('click', resetGame); | |
| hintButton.addEventListener('click', getAiHint); | |
| resetGame(); | |
| } | |
| function initMyComputer(windowElement: HTMLDivElement): void { | |
| const cDriveIcon = windowElement.querySelector('#c-drive-icon') as HTMLDivElement; | |
| const cDriveContent = windowElement.querySelector('#c-drive-content') as HTMLDivElement; | |
| const secretImageIcon = windowElement.querySelector('#secret-image-icon') as HTMLDivElement; | |
| if (!cDriveIcon || !cDriveContent || !secretImageIcon) return; | |
| cDriveIcon.addEventListener('click', () => { | |
| cDriveIcon.style.display = 'none'; cDriveContent.style.display = 'block'; | |
| }); | |
| secretImageIcon.addEventListener('click', () => { | |
| const imageViewerWindow = document.getElementById('imageViewer') as HTMLDivElement | null; | |
| const imageViewerImg = document.getElementById('image-viewer-img') as HTMLImageElement | null; | |
| const imageViewerTitle = document.getElementById('image-viewer-title') as HTMLSpanElement | null; | |
| if (!imageViewerWindow || !imageViewerImg || !imageViewerTitle) { alert("Image Viewer corrupted!"); return; } | |
| imageViewerImg.src = 'https://storage.googleapis.com/gemini-95-icons/%40ammaar%2B%40olacombe.png'; | |
| imageViewerImg.alt = 'dontshowthistoanyone.jpg'; | |
| imageViewerTitle.textContent = 'dontshowthistoanyone.jpg - Image Viewer'; | |
| openApp('imageViewer'); | |
| }); | |
| cDriveIcon.style.display = 'inline-flex'; cDriveContent.style.display = 'none'; | |
| } | |
| // --- YouTube Player (GemPlayer) Logic --- | |
| function loadYouTubeApi(): Promise<void> { | |
| if (ytApiLoaded) return Promise.resolve(); | |
| if (ytApiLoadingPromise) return ytApiLoadingPromise; | |
| ytApiLoadingPromise = new Promise((resolve, reject) => { | |
| // @ts-ignore | |
| if (window.YT && window.YT.Player) { | |
| ytApiLoaded = true; resolve(); return; | |
| } | |
| const tag = document.createElement('script'); | |
| tag.src = "https://www.youtube.com/iframe_api"; | |
| tag.onerror = (err) => { | |
| console.error("Failed to load YouTube API script:", err); | |
| ytApiLoadingPromise = null; | |
| reject(new Error("YouTube API script load failed")); | |
| }; | |
| const firstScriptTag = document.getElementsByTagName('script')[0]; | |
| firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); | |
| // @ts-ignore | |
| window.onYouTubeIframeAPIReady = () => { | |
| ytApiLoaded = true; ytApiLoadingPromise = null; resolve(); | |
| }; | |
| setTimeout(() => { | |
| if (!ytApiLoaded) { | |
| // @ts-ignore | |
| if (window.onYouTubeIframeAPIReady) window.onYouTubeIframeAPIReady = null; | |
| ytApiLoadingPromise = null; | |
| reject(new Error("YouTube API load timeout")); | |
| } | |
| }, 10000); | |
| }); | |
| return ytApiLoadingPromise; | |
| } | |
| function getYouTubeVideoId(urlOrId: string): string | null { | |
| if (!urlOrId) return null; | |
| if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) return urlOrId; | |
| const regExp = /^.*(?:youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]{11}).*/; | |
| const match = urlOrId.match(regExp); | |
| return (match && match[1]) ? match[1] : null; | |
| } | |
| async function initMediaPlayer(windowElement: HTMLDivElement): Promise<void> { | |
| const appName = windowElement.id; // 'mediaPlayer' | |
| const urlInput = windowElement.querySelector('.media-player-input') as HTMLInputElement; | |
| const loadButton = windowElement.querySelector('.media-player-load-button') as HTMLButtonElement; | |
| const playerContainerDivId = `youtube-player-${appName}`; | |
| const playerDiv = windowElement.querySelector(`#${playerContainerDivId}`) as HTMLDivElement; | |
| const playButton = windowElement.querySelector('#media-player-play') as HTMLButtonElement; | |
| const pauseButton = windowElement.querySelector('#media-player-pause') as HTMLButtonElement; | |
| const stopButton = windowElement.querySelector('#media-player-stop') as HTMLButtonElement; | |
| if (!urlInput || !loadButton || !playerDiv || !playButton || !pauseButton || !stopButton) { | |
| console.error("Media Player elements not found for", appName); | |
| if (playerDiv) playerDiv.innerHTML = `<p class="media-player-status-message" style="color:red;">Error: Player UI missing.</p>`; | |
| return; | |
| } | |
| const updateButtonStates = (playerState?: number) => { | |
| // @ts-ignore | |
| const YTPlayerState = window.YT?.PlayerState; | |
| if (!YTPlayerState) { | |
| playButton.disabled = true; pauseButton.disabled = true; stopButton.disabled = true; | |
| return; | |
| } | |
| const state = playerState !== undefined ? playerState | |
| // @ts-ignore | |
| : (youtubePlayers[appName] && typeof youtubePlayers[appName].getPlayerState === 'function' ? youtubePlayers[appName].getPlayerState() : YTPlayerState.UNSTARTED); | |
| playButton.disabled = state === YTPlayerState.PLAYING || state === YTPlayerState.BUFFERING; | |
| pauseButton.disabled = state !== YTPlayerState.PLAYING && state !== YTPlayerState.BUFFERING; // Can pause if buffering too | |
| stopButton.disabled = state === YTPlayerState.ENDED || state === YTPlayerState.UNSTARTED || state === -1 /* UNSTARTED also seen as -1 */; | |
| }; | |
| updateButtonStates(-1); // Initial state (unstarted) | |
| const showPlayerMessage = (message: string, isError: boolean = false) => { | |
| const player = youtubePlayers[appName]; | |
| if (player) { | |
| try { if (typeof player.destroy === 'function') player.destroy(); } | |
| catch(e) { console.warn("Minor error destroying player:", e); } | |
| delete youtubePlayers[appName]; | |
| } | |
| playerDiv.innerHTML = `<p class="media-player-status-message" style="color:${isError ? 'red' : '#ccc'};">${message}</p>`; | |
| updateButtonStates(-1); | |
| }; | |
| const initialStatusMessageEl = playerDiv.querySelector('.media-player-status-message'); | |
| if (initialStatusMessageEl) initialStatusMessageEl.textContent = 'Connecting to YouTube...'; | |
| try { | |
| await loadYouTubeApi(); | |
| if (initialStatusMessageEl) initialStatusMessageEl.textContent = 'YouTube API Ready. Loading default video...'; | |
| } catch (error: any) { | |
| showPlayerMessage(`Error: Could not load YouTube Player API. ${error.message}`, true); | |
| return; | |
| } | |
| const createPlayer = (videoId: string) => { | |
| const existingPlayer = youtubePlayers[appName]; | |
| if (existingPlayer) { | |
| try { if (typeof existingPlayer.destroy === 'function') existingPlayer.destroy(); } | |
| catch(e) { console.warn("Minor error destroying previous player:", e); } | |
| } | |
| playerDiv.innerHTML = ''; // Clear previous content/message | |
| try { | |
| // @ts-ignore | |
| youtubePlayers[appName] = new YT.Player(playerContainerDivId, { | |
| height: '100%', width: '100%', videoId: videoId, | |
| playerVars: { 'playsinline': 1, 'autoplay': 1, 'controls': 0, 'modestbranding': 1, 'rel': 0, 'fs': 0, 'origin': window.location.origin }, | |
| events: { | |
| 'onReady': (event: any) => { /* Autoplay handles start */ updateButtonStates(event.target.getPlayerState()); }, | |
| 'onError': (event: any) => { | |
| const errorMessages: { [key: number]: string } = { 2: "Invalid video ID.", 5: "HTML5 Player error.", 100: "Video not found/private.", 101: "Playback disallowed.", 150: "Playback disallowed."}; | |
| showPlayerMessage(errorMessages[event.data] || `Playback Error (Code: ${event.data})`, true); | |
| }, | |
| 'onStateChange': (event: any) => { updateButtonStates(event.data); } | |
| } | |
| }); | |
| } catch (error: any) { | |
| showPlayerMessage(`Failed to create video player: ${error.message}`, true); | |
| } | |
| }; | |
| loadButton.addEventListener('click', () => { | |
| const videoUrlOrId = urlInput.value.trim(); | |
| const videoId = getYouTubeVideoId(videoUrlOrId); | |
| if (videoId) { | |
| showPlayerMessage("Loading video..."); // Show loading message immediately | |
| createPlayer(videoId); | |
| } else { | |
| showPlayerMessage("Invalid YouTube URL or Video ID.", true); | |
| } | |
| }); | |
| playButton.addEventListener('click', () => { | |
| const player = youtubePlayers[appName]; | |
| // @ts-ignore | |
| if (player && typeof player.playVideo === 'function') player.playVideo(); | |
| }); | |
| pauseButton.addEventListener('click', () => { | |
| const player = youtubePlayers[appName]; | |
| // @ts-ignore | |
| if (player && typeof player.pauseVideo === 'function') player.pauseVideo(); | |
| }); | |
| stopButton.addEventListener('click', () => { | |
| const player = youtubePlayers[appName]; | |
| // @ts-ignore | |
| if (player && typeof player.stopVideo === 'function') { | |
| player.stopVideo(); | |
| // @ts-ignore - Manually set to ended for button state update | |
| updateButtonStates(window.YT?.PlayerState?.ENDED); | |
| } | |
| }); | |
| if (DEFAULT_YOUTUBE_VIDEO_ID) { | |
| if (initialStatusMessageEl) initialStatusMessageEl.textContent = `Loading default video...`; // Update message | |
| createPlayer(DEFAULT_YOUTUBE_VIDEO_ID); | |
| } else { | |
| // Message already set by HTML if no default video. | |
| // showPlayerMessage("Enter a YouTube URL or Video ID and click 'Load'."); | |
| } | |
| } | |
| // --- END YouTube Player Logic --- | |
| async function initializeGeminiIfNeeded(context: string): Promise<boolean> { | |
| if (geminiInstance) return true; | |
| try { | |
| const module = await import('@google/genai'); | |
| // @ts-ignore | |
| const GoogleAIClass = module.GoogleGenAI; | |
| if (typeof GoogleAIClass !== 'function') throw new Error("GoogleGenAI constructor not found."); | |
| // @ts-ignore | |
| const apiKey = process.env.GEMINI_API_KEY || ""; | |
| if (!apiKey) { | |
| alert("CRITICAL ERROR: Gemini API Key missing."); | |
| throw new Error("API Key is missing."); | |
| } | |
| // @ts-ignore | |
| geminiInstance = new GoogleAIClass({apiKey: apiKey}); | |
| return true; | |
| } catch (error: any) { | |
| console.error(`Failed Gemini initialization in ${context}:`, error); | |
| alert(`CRITICAL ERROR: Gemini AI failed to initialize. ${error.message}`); | |
| return false; | |
| } | |
| } | |