// ===================================================================== // Sulfur Launcher — Core Logic & Instance Engine // ===================================================================== const STORAGE_KEY = 'sulfur:state'; const DB_NAME = 'SulfurLauncherDB'; const DB_VERSION = 1; // --- 1. Default Instances Catalog -------------------------------------- const DEFAULT_INSTANCES = [ { id: 'sulfur-v25', title: 'Sulfur Client v2.5', icon: 'assets/thumb-atm10.png', bg: 'assets/bg-atm10.jpg', tags: ['2.5', 'SULFUR', 'FPS-BOOST'], playtimeSeconds: 7200, lastPlayed: new Date(Date.now() - 3600 * 2000).toISOString(), launchesCount: 14, launchType: 'local', launchFile: 'instances/sulfur.html' }, { id: 'resent-188', title: 'Resent Client 1.8.8', icon: 'assets/thumb-vanilla8.png', bg: 'assets/bg-vanilla8.jpg', tags: ['1.8.8', 'MODDED', 'PVP'], playtimeSeconds: 14400, lastPlayed: new Date(Date.now() - 3600 * 24000).toISOString(), launchesCount: 28, launchType: 'local', launchFile: 'instances/resent_client.html' }, { id: 'vanilla-188', title: 'Vanilla 1.8.8', icon: 'assets/thumb-vanilla8.png', bg: 'assets/bg-vanilla8.jpg', tags: ['1.8.8', 'VANILLA', 'MULTIPLAYER'], playtimeSeconds: 43200, lastPlayed: new Date(Date.now() - 3600 * 48000).toISOString(), launchesCount: 52, launchType: 'local', launchFile: 'instances/eagler_1_8_8.html' } ]; // Curated Collection Presets catalog const CURATED_PRESETS = [ { id: 'preset-sulfur', title: 'Sulfur Client v2.5 Obsidian', version: '2.5', description: 'Official Sulfur web client with WebGL shader optimization and server list.', icon: 'assets/thumb-atm10.png', bg: 'assets/bg-atm10.jpg', tags: ['2.5', 'SULFUR', 'OFFICIAL'], launchType: 'local', launchFile: 'instances/sulfur.html' }, { id: 'preset-resent', title: 'Resent Client 1.8.8', version: '1.8.8', description: 'Popular PvP modpack client built for Eaglercraft 1.8.8.', icon: 'assets/thumb-vanilla8.png', bg: 'assets/bg-vanilla8.jpg', tags: ['1.8.8', 'PVP', 'MODDED'], launchType: 'local', launchFile: 'instances/resent_client.html' }, { id: 'preset-vanilla', title: 'Vanilla Eagler 1.8.8', version: '1.8.8', description: 'Clean standard Eaglercraft 1.8.8 vanilla client build.', icon: 'assets/thumb-vanilla8.png', bg: 'assets/bg-vanilla8.jpg', tags: ['1.8.8', 'VANILLA', 'CLEAN'], launchType: 'local', launchFile: 'instances/eagler_1_8_8.html' } ]; // --- 2. IndexedDB Storage Manager ------------------------------------- class SulfurDBManager { constructor() { this.db = null; } async init() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains('instances_files')) { db.createObjectStore('instances_files', { keyPath: 'id' }); } }; req.onsuccess = e => { this.db = e.target.result; resolve(this.db); }; req.onerror = e => reject(e.target.error); }); } async saveClientFile(fileId, content) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const tx = this.db.transaction('instances_files', 'readwrite'); const store = tx.objectStore('instances_files'); const req = store.put({ id: fileId, content, updatedAt: Date.now() }); req.onsuccess = () => resolve(fileId); req.onerror = e => reject(e.target.error); }); } async getClientFile(fileId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const tx = this.db.transaction('instances_files', 'readonly'); const store = tx.objectStore('instances_files'); const req = store.get(fileId); req.onsuccess = () => resolve(req.result ? req.result.content : null); req.onerror = e => reject(e.target.error); }); } async deleteClientFile(fileId) { if (!this.db) await this.init(); return new Promise((resolve, reject) => { const tx = this.db.transaction('instances_files', 'readwrite'); const store = tx.objectStore('instances_files'); const req = store.delete(fileId); req.onsuccess = () => resolve(); req.onerror = e => reject(e.target.error); }); } } const dbManager = new SulfurDBManager(); // --- 3. State Management ----------------------------------------------- function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) throw new Error('no state'); const parsed = JSON.parse(raw); return { instances: parsed.instances && parsed.instances.length ? parsed.instances : DEFAULT_INSTANCES, activeInstanceId: parsed.activeInstanceId || DEFAULT_INSTANCES[0].id, onboardingCompleted: parsed.onboardingCompleted || false, settings: Object.assign({ reduceMotion: false, compactSidebar: false }, parsed.settings || {}) }; } catch (e) { return { instances: DEFAULT_INSTANCES, activeInstanceId: DEFAULT_INSTANCES[0].id, onboardingCompleted: false, settings: { reduceMotion: false, compactSidebar: false } }; } } function saveState() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { /* fail-safe */ } } const state = loadState(); let activeSession = null; let deferredPrompt = null; // PWA install prompt // --- 4. Utilities & Formatters ----------------------------------------- function toast(message) { const container = document.getElementById('toast-container'); const el = document.createElement('div'); el.className = 'toast'; el.textContent = message; container.appendChild(el); setTimeout(() => { el.classList.add('leaving'); setTimeout(() => el.remove(), 220); }, 3200); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str || ''; return div.innerHTML; } function formatPlaytime(seconds) { if (!seconds || seconds <= 0) return 'Never played'; const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); if (hrs > 0) return `${hrs}h ${mins}m played`; return `${mins}m played`; } function formatLastPlayed(timestamp) { if (!timestamp) return 'Never'; const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHrs = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 2) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHrs < 24) return `${diffHrs}h ago`; if (diffDays === 1) return 'Yesterday'; return `${diffDays}d ago`; } // --- 5. Startup Animation ---------------------------------------------- function runStartup() { const startupScreen = document.getElementById('startup-screen'); const status = document.getElementById('startup-status'); const messages = ['Loading instances…', 'Initializing Sulfur engine…', 'Preparing stage…']; let i = 0; if (state.settings.reduceMotion) { startupScreen.classList.add('removed'); checkOnboarding(); return; } const msgInterval = setInterval(() => { i = (i + 1) % messages.length; status.textContent = messages[i]; }, 600); setTimeout(() => { clearInterval(msgInterval); startupScreen.classList.add('wipe-out'); setTimeout(() => { startupScreen.classList.add('removed'); checkOnboarding(); }, 900); }, 1800); } function checkOnboarding() { if (!state.onboardingCompleted) { openModal('modal-onboarding'); } } // --- 6. Sidebar & Stage Renderers --------------------------------------- function renderSidebar() { const sidebarList = document.getElementById('sidebar-instances'); sidebarList.innerHTML = ''; state.instances.forEach(inst => { const iconDiv = document.createElement('div'); iconDiv.className = `instance-icon ${inst.id === state.activeInstanceId ? 'active' : ''}`; iconDiv.style.backgroundImage = `url('${inst.icon}')`; iconDiv.title = inst.title; iconDiv.onclick = () => selectInstance(inst.id, true); sidebarList.appendChild(iconDiv); }); } function renderLibrary() { const grid = document.getElementById('library-grid'); grid.innerHTML = ''; state.instances.forEach(inst => { const card = document.createElement('div'); card.className = `library-card ${inst.id === state.activeInstanceId ? 'active' : ''}`; card.style.backgroundImage = `url('${inst.bg}')`; card.innerHTML = `

${escapeHtml(inst.title)}

${inst.tags.join(' · ')} • ${formatPlaytime(inst.playtimeSeconds)}
`; card.onclick = () => { selectInstance(inst.id, true); switchView('play'); }; grid.appendChild(card); }); } function renderDiscoverCatalog() { const catalog = document.getElementById('discover-catalog'); catalog.innerHTML = ''; CURATED_PRESETS.forEach(preset => { const card = document.createElement('div'); card.className = 'discover-card'; const isInstalled = state.instances.some(i => i.title === preset.title); card.innerHTML = `

${escapeHtml(preset.title)}

${escapeHtml(preset.description)}

${preset.tags.map(t => `${t}`).join('')}
`; catalog.appendChild(card); if (!isInstalled) { document.getElementById(`btn-install-${preset.id}`).onclick = () => installCuratedPreset(preset); } }); } function selectInstance(id, persist) { state.activeInstanceId = id; renderSidebar(); renderLibrary(); loadInstance(id); if (persist) saveState(); } function loadInstance(id) { const inst = state.instances.find(i => i.id === id); if (!inst) return; document.getElementById('background-layer').style.backgroundImage = `url('${inst.bg}')`; document.getElementById('instance-title').innerText = inst.title; const playtimeText = activeSession && activeSession.instanceId === id ? `Playing now • ${formatPlaytime(inst.playtimeSeconds)}` : `${formatPlaytime(inst.playtimeSeconds)} • Last played ${formatLastPlayed(inst.lastPlayed)}`; document.getElementById('instance-playtime').innerText = playtimeText; const tagsContainer = document.getElementById('instance-tags'); tagsContainer.innerHTML = ''; inst.tags.forEach(tag => { const tagSpan = document.createElement('span'); tagSpan.className = 'tag'; tagSpan.innerText = tag; tagsContainer.appendChild(tagSpan); }); } function switchView(view) { document.querySelectorAll('.nav-item[data-view]').forEach(btn => { btn.classList.toggle('active', btn.dataset.view === view); }); document.querySelectorAll('.view').forEach(section => { section.classList.toggle('active', section.id === `view-${view}`); }); if (view === 'library') renderLibrary(); if (view === 'discover') renderDiscoverCatalog(); } // --- 7. Play Button & Session Engine ----------------------------------- async function initPlayButton() { document.getElementById('play-btn').addEventListener('click', async () => { const inst = state.instances.find(i => i.id === state.activeInstanceId); if (!inst) return; const btn = document.getElementById('play-btn'); btn.disabled = true; btn.innerHTML = ' LAUNCHING…'; let targetUrl = inst.launchFile; // If local IndexedDB client file, resolve Blob URL if (inst.launchType === 'db' || targetUrl.startsWith('db:')) { const fileId = targetUrl.replace('db:', ''); const htmlContent = await dbManager.getClientFile(fileId); if (!htmlContent) { toast('Error: Local client HTML file not found in database.'); btn.disabled = false; btn.innerHTML = ' PLAY'; return; } const blob = new Blob([htmlContent], { type: 'text/html' }); targetUrl = URL.createObjectURL(blob); } setTimeout(() => { const win = window.open( targetUrl, `SulfurClient_${inst.id}`, 'popup=1,width=1280,height=720,menubar=no,toolbar=no,location=no,status=no' ); if (!win) { toast('Browser blocked popup window — allow popups for Sulfur.'); btn.disabled = false; btn.innerHTML = ' PLAY'; return; } // Start Session Tracking inst.launchesCount = (inst.launchesCount || 0) + 1; inst.lastPlayed = new Date().toISOString(); saveState(); startPlaytimeSession(inst, win); btn.disabled = false; btn.innerHTML = ' PLAYING'; }, 500); }); } function startPlaytimeSession(inst, win) { if (activeSession) clearInterval(activeSession.timerId); const startTime = Date.now(); const timerId = setInterval(() => { if (win.closed) { clearInterval(timerId); const durationSec = Math.floor((Date.now() - startTime) / 1000); inst.playtimeSeconds = (inst.playtimeSeconds || 0) + durationSec; inst.lastPlayed = new Date().toISOString(); saveState(); activeSession = null; loadInstance(state.activeInstanceId); renderLibrary(); toast(`Session finished — played ${inst.title} for ${formatPlaytime(durationSec)}.`); } else { inst.playtimeSeconds = (inst.playtimeSeconds || 0) + 1; if (state.activeInstanceId === inst.id) { document.getElementById('instance-playtime').innerText = `Playing now • ${formatPlaytime(inst.playtimeSeconds)}`; } } }, 1000); activeSession = { instanceId: inst.id, startTime, timerId, win }; } // --- 8. Instance Manager (Add, Edit, Delete, Collection Model) -------- let pendingLocalFile = null; let pendingJsonCollection = null; function initAddInstanceModal() { // Open Add Modal document.getElementById('btn-sidebar-add').addEventListener('click', () => openModal('modal-add-instance')); document.getElementById('btn-lib-add').addEventListener('click', () => openModal('modal-add-instance')); // Modal Tabs document.querySelectorAll('.modal-tab').forEach(tabBtn => { tabBtn.addEventListener('click', () => { document.querySelectorAll('.modal-tab').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tabBtn.classList.add('active'); document.getElementById(tabBtn.dataset.tab).classList.add('active'); }); }); // Local File Drop Zone const dropZone = document.getElementById('file-drop-zone'); const fileInput = document.getElementById('local-file-input'); fileInput.addEventListener('change', e => { if (e.target.files && e.target.files[0]) { handleSelectedLocalFile(e.target.files[0]); } }); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleSelectedLocalFile(e.dataTransfer.files[0]); } }); function handleSelectedLocalFile(file) { if (!file.name.endsWith('.html') && !file.name.endsWith('.htm')) { toast('Please select an .html or .htm file.'); return; } pendingLocalFile = file; document.getElementById('drop-filename').innerText = `Selected: ${file.name} (${Math.round(file.size / 1024)} KB)`; if (!document.getElementById('add-local-title').value.trim()) { document.getElementById('add-local-title').value = file.name.replace(/\.[^/.]+$/, ''); } } // Confirm Add Local document.getElementById('confirm-add-local').addEventListener('click', async () => { if (!pendingLocalFile) { toast('Please select a local HTML file first.'); return; } const title = document.getElementById('add-local-title').value.trim() || 'Local Client'; const tags = document.getElementById('add-local-tags').value.split(',').map(t => t.trim().toUpperCase()).filter(Boolean); const version = document.getElementById('add-local-version').value.trim() || '1.8.8'; const fileId = `file_${Date.now()}`; const htmlText = await pendingLocalFile.text(); await dbManager.saveClientFile(fileId, htmlText); const newInst = { id: `inst_${Date.now()}`, title, tags, version, icon: 'assets/thumb-atm10.png', bg: 'assets/bg-atm10.jpg', playtimeSeconds: 0, lastPlayed: null, launchesCount: 0, launchType: 'db', launchFile: `db:${fileId}` }; state.instances.push(newInst); saveState(); selectInstance(newInst.id, true); closeModal('modal-add-instance'); toast(`Instance "${title}" added!`); pendingLocalFile = null; document.getElementById('drop-filename').innerText = 'Click or Drag & Drop local .html client file here'; }); // Confirm Add Hosted document.getElementById('confirm-add-hosted').addEventListener('click', () => { const url = document.getElementById('add-hosted-url').value.trim(); if (!url) { toast('Please enter a launch URL.'); return; } const title = document.getElementById('add-hosted-title').value.trim() || 'Hosted Client'; const tags = document.getElementById('add-hosted-tags').value.split(',').map(t => t.trim().toUpperCase()).filter(Boolean); const version = document.getElementById('add-hosted-version').value.trim() || '1.8.8'; const newInst = { id: `inst_${Date.now()}`, title, tags, version, icon: 'assets/thumb-vanilla8.png', bg: 'assets/bg-vanilla8.jpg', playtimeSeconds: 0, lastPlayed: null, launchesCount: 0, launchType: 'url', launchFile: url }; state.instances.push(newInst); saveState(); selectInstance(newInst.id, true); closeModal('modal-add-instance'); toast(`Hosted instance "${title}" added!`); }); // Populate Curated Collection Tab renderPresetCollectionTab(); // Import Collection JSON tab logic const jsonInput = document.getElementById('json-file-input'); jsonInput.addEventListener('change', e => { if (e.target.files && e.target.files[0]) handleSelectedJson(e.target.files[0]); }); function handleSelectedJson(file) { const reader = new FileReader(); reader.onload = e => { try { const data = JSON.parse(e.target.result); if (!data.instances || !Array.isArray(data.instances)) throw new Error('Invalid schema'); pendingJsonCollection = data; document.getElementById('json-filename').innerText = `Selected: ${file.name}`; document.getElementById('preview-collection-name').innerText = data.name || 'Custom Collection'; document.getElementById('preview-collection-count').innerText = `${data.instances.length} instances ready to import`; document.getElementById('json-import-preview').classList.remove('hidden'); document.getElementById('confirm-import-json').disabled = false; } catch (err) { toast('Invalid Sulfur collection JSON file.'); } }; reader.readAsText(file); } document.getElementById('confirm-import-json').addEventListener('click', () => { if (!pendingJsonCollection) return; let addedCount = 0; pendingJsonCollection.instances.forEach(item => { const exists = state.instances.some(i => i.id === item.id || i.title === item.title); if (!exists) { state.instances.push({ id: item.id || `inst_${Date.now()}_${Math.random()}`, title: item.title || 'Imported Client', tags: item.tags || ['IMPORTED'], icon: item.icon || 'assets/thumb-atm10.png', bg: item.bg || 'assets/bg-atm10.jpg', playtimeSeconds: item.playtimeSeconds || 0, lastPlayed: item.lastPlayed || null, launchesCount: item.launchesCount || 0, launchType: item.launchType || 'local', launchFile: item.launchFile || 'instances/sulfur.html' }); addedCount++; } }); saveState(); renderSidebar(); renderLibrary(); closeModal('modal-add-instance'); toast(`Imported ${addedCount} instances from collection!`); }); } function renderPresetCollectionTab() { const container = document.getElementById('preset-collection-list'); container.innerHTML = ''; CURATED_PRESETS.forEach(preset => { const item = document.createElement('div'); item.className = 'collection-item'; item.innerHTML = `
${escapeHtml(preset.title)}

${preset.tags.join(' · ')} • ${escapeHtml(preset.description)}

`; container.appendChild(item); document.getElementById(`btn-add-preset-${preset.id}`).onclick = () => installCuratedPreset(preset); }); } function installCuratedPreset(preset) { const exists = state.instances.some(i => i.title === preset.title); if (exists) { toast('Instance is already in your launcher!'); return; } const newInst = { id: `inst_${Date.now()}`, title: preset.title, tags: preset.tags, icon: preset.icon, bg: preset.bg, playtimeSeconds: 0, lastPlayed: null, launchesCount: 0, launchType: preset.launchType, launchFile: preset.launchFile }; state.instances.push(newInst); saveState(); selectInstance(newInst.id, true); closeModal('modal-add-instance'); toast(`Added ${preset.title}!`); } // --- 9. Edit & Delete Instance ----------------------------------------- function initEditModal() { document.getElementById('btn-edit').addEventListener('click', () => { const inst = state.instances.find(i => i.id === state.activeInstanceId); if (!inst) return; document.getElementById('edit-title').value = inst.title; document.getElementById('edit-tags').value = inst.tags.join(', '); document.getElementById('edit-launch-file').value = inst.launchFile; openModal('modal-edit'); }); document.getElementById('confirm-edit').addEventListener('click', () => { const inst = state.instances.find(i => i.id === state.activeInstanceId); if (!inst) return; const newTitle = document.getElementById('edit-title').value.trim(); const newTags = document.getElementById('edit-tags').value .split(',').map(t => t.trim().toUpperCase()).filter(Boolean); const newTarget = document.getElementById('edit-launch-file').value.trim(); if (newTitle) inst.title = newTitle; if (newTags.length) inst.tags = newTags; if (newTarget) inst.launchFile = newTarget; loadInstance(inst.id); renderSidebar(); renderLibrary(); saveState(); closeModal('modal-edit'); toast('Instance saved.'); }); // Delete Instance document.getElementById('btn-delete-instance').addEventListener('click', () => { const inst = state.instances.find(i => i.id === state.activeInstanceId); if (!inst) return; document.getElementById('delete-instance-name').innerText = inst.title; closeModal('modal-edit'); openModal('modal-confirm-delete'); }); document.getElementById('confirm-delete-action').addEventListener('click', async () => { const instId = state.activeInstanceId; const inst = state.instances.find(i => i.id === instId); if (inst && (inst.launchType === 'db' || inst.launchFile.startsWith('db:'))) { const fileId = inst.launchFile.replace('db:', ''); await dbManager.deleteClientFile(fileId); } state.instances = state.instances.filter(i => i.id !== instId); if (!state.instances.length) state.instances = DEFAULT_INSTANCES; state.activeInstanceId = state.instances[0].id; saveState(); renderSidebar(); renderLibrary(); loadInstance(state.activeInstanceId); closeModal('modal-confirm-delete'); toast('Instance deleted.'); }); } // --- 10. Collection Model JSON Export/Import ---------------------------- function exportCollectionJson() { const collectionData = { schemaVersion: '1.0', name: 'Sulfur Local Collection', exportedAt: new Date().toISOString(), instances: state.instances.map(inst => ({ id: inst.id, title: inst.title, tags: inst.tags, icon: inst.icon, bg: inst.bg, launchType: inst.launchType, launchFile: inst.launchType === 'db' ? 'instances/sulfur.html' : inst.launchFile, playtimeSeconds: inst.playtimeSeconds || 0, lastPlayed: inst.lastPlayed || null })) }; const blob = new Blob([JSON.stringify(collectionData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'sulfur-collection.json'; a.click(); URL.revokeObjectURL(url); toast('Exported sulfur-collection.json!'); } // --- 11. Onboarding Wizard & PWA Setup --------------------------------- let currentOnboardingStep = 1; function initOnboardingWizard() { document.getElementById('btn-onboarding').addEventListener('click', () => { currentOnboardingStep = 1; updateOnboardingStep(); openModal('modal-onboarding'); }); document.getElementById('setting-onboarding-btn').addEventListener('click', () => { closeModal('modal-settings'); currentOnboardingStep = 1; updateOnboardingStep(); openModal('modal-onboarding'); }); document.getElementById('onboarding-next').addEventListener('click', () => { if (currentOnboardingStep < 4) { currentOnboardingStep++; updateOnboardingStep(); } else { finishOnboarding(); } }); document.getElementById('onboarding-prev').addEventListener('click', () => { if (currentOnboardingStep > 1) { currentOnboardingStep--; updateOnboardingStep(); } }); document.getElementById('onboarding-skip').addEventListener('click', finishOnboarding); // Capture PWA deferred prompt window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); deferredPrompt = e; }); document.getElementById('pwa-install-trigger').addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { toast('Sulfur Desktop App installed!'); } deferredPrompt = null; } else { toast('To install: click your browser URL bar install button or Share → Add to Home Screen.'); } }); } function updateOnboardingStep() { document.querySelectorAll('.step-indicator').forEach(ind => { const s = parseInt(ind.dataset.step); ind.classList.toggle('active', s <= currentOnboardingStep); }); document.querySelectorAll('.onboarding-slide').forEach((slide, idx) => { slide.classList.toggle('active', (idx + 1) === currentOnboardingStep); }); document.getElementById('onboarding-prev').style.visibility = currentOnboardingStep > 1 ? 'visible' : 'hidden'; const nextBtn = document.getElementById('onboarding-next'); nextBtn.innerHTML = currentOnboardingStep === 4 ? 'Get Started ✓' : 'Next →'; } function finishOnboarding() { state.onboardingCompleted = true; saveState(); closeModal('modal-onboarding'); toast('Welcome to Sulfur Launcher!'); } // --- 12. Stats & Analytics Panel --------------------------------------- function initStatsModal() { document.getElementById('btn-stats').addEventListener('click', () => { renderStatsData(); openModal('modal-stats'); }); } function renderStatsData() { const totalSec = state.instances.reduce((acc, i) => acc + (i.playtimeSeconds || 0), 0); const totalLaunches = state.instances.reduce((acc, i) => acc + (i.launchesCount || 0), 0); const sortedByTime = [...state.instances].sort((a, b) => (b.playtimeSeconds || 0) - (a.playtimeSeconds || 0)); const mostPlayed = sortedByTime[0] ? sortedByTime[0].title : 'None'; document.getElementById('stat-total-playtime').innerText = formatPlaytime(totalSec); document.getElementById('stat-total-launches').innerText = totalLaunches.toString(); document.getElementById('stat-most-played').innerText = mostPlayed; document.getElementById('stat-instance-count').innerText = state.instances.length.toString(); const activityList = document.getElementById('stats-instance-list'); activityList.innerHTML = ''; sortedByTime.forEach(inst => { const item = document.createElement('div'); item.className = 'activity-item'; item.innerHTML = `
${escapeHtml(inst.title)}
${formatPlaytime(inst.playtimeSeconds)} • ${inst.launchesCount || 0} launches
`; activityList.appendChild(item); }); } // --- 13. Settings & Window Controls ------------------------------------ function initSettingsModal() { const reduceMotionInput = document.getElementById('setting-reduce-motion'); const compactSidebarInput = document.getElementById('setting-compact-sidebar'); reduceMotionInput.checked = state.settings.reduceMotion; compactSidebarInput.checked = state.settings.compactSidebar; document.body.classList.toggle('compact-sidebar', state.settings.compactSidebar); document.getElementById('btn-settings').addEventListener('click', () => openModal('modal-settings')); reduceMotionInput.addEventListener('change', () => { state.settings.reduceMotion = reduceMotionInput.checked; saveState(); }); compactSidebarInput.addEventListener('change', () => { state.settings.compactSidebar = compactSidebarInput.checked; document.body.classList.toggle('compact-sidebar', state.settings.compactSidebar); saveState(); }); document.getElementById('setting-export-btn').addEventListener('click', exportCollectionJson); document.getElementById('btn-lib-export').addEventListener('click', exportCollectionJson); document.getElementById('btn-changelog').addEventListener('click', () => { toast('v2.5 — Instance Manager, Playtime Tracker, Local Files & PWA Onboarding!'); }); } function initWindowControls() { const pill = document.getElementById('minimized-pill'); document.getElementById('btn-minimize').addEventListener('click', () => { document.body.classList.add('minimized'); setTimeout(() => pill.classList.remove('hidden'), 250); try { window.blur(); } catch (e) {} }); pill.addEventListener('click', () => { pill.classList.add('hidden'); document.body.classList.remove('minimized'); try { window.focus(); } catch (e) {} }); const maxBtn = document.getElementById('btn-maximize'); const maxIconUse = document.querySelector('#maximize-icon use'); maxBtn.addEventListener('click', async () => { try { if (!document.fullscreenElement) { await document.documentElement.requestFullscreen(); } else { await document.exitFullscreen(); } } catch (e) { toast('Browser blocked fullscreen.'); } }); document.addEventListener('fullscreenchange', () => { const isFull = !!document.fullscreenElement; maxIconUse.setAttribute('href', isFull ? '#icon-restore' : '#icon-maximize'); maxBtn.title = isFull ? 'Restore' : 'Maximize'; }); document.getElementById('btn-close').addEventListener('click', () => openModal('modal-close')); document.getElementById('confirm-close').addEventListener('click', () => { closeModal('modal-close'); window.close(); setTimeout(() => { toast("Press Ctrl+W (⌘W on Mac) to close this browser tab."); }, 300); }); } // Global modal helpers function openModal(id) { document.getElementById(id).classList.add('visible'); } function closeModal(id) { document.getElementById(id).classList.remove('visible'); } document.addEventListener('click', e => { if (e.target.dataset && e.target.dataset.closeModal) closeModal(e.target.dataset.closeModal); if (e.target.classList.contains('modal-overlay')) e.target.classList.remove('visible'); }); // --- 14. Boot Application ----------------------------------------------- document.addEventListener('DOMContentLoaded', async () => { await dbManager.init(); runStartup(); renderSidebar(); renderLibrary(); loadInstance(state.activeInstanceId); // Bind nav views document.querySelectorAll('.nav-item[data-view]').forEach(btn => { btn.addEventListener('click', () => switchView(btn.dataset.view)); }); initPlayButton(); initAddInstanceModal(); initEditModal(); initOnboardingWizard(); initStatsModal(); initSettingsModal(); initWindowControls(); });