diff --git a/main.js b/main.js index 74de7ad..44b274f 100644 --- a/main.js +++ b/main.js @@ -459,6 +459,130 @@ function getZoomTargetForEvent(event) { return win.webContents; } +// ============================================================================= +// FIRST-TIME SETUP UTILITIES +// ============================================================================= + +/** + * Check if this is the first run of the application + */ +function getOnboardingFilePath() { + try { + const portablePath = portableData.getDataFilePath?.('first-run.json'); + if (portablePath) return portablePath; + } catch {} + return path.join(app.getPath('userData'), 'first-run.json'); +} + +function migrateFirstRunFile() { + const newPath = getOnboardingFilePath(); + const legacyPath = path.join(__dirname, 'first-run.json'); + if (newPath === legacyPath) return; + try { + if (!fs.existsSync(newPath) && fs.existsSync(legacyPath)) { + const data = fs.readFileSync(legacyPath, 'utf8'); + fs.writeFileSync(newPath, data); + try { fs.unlinkSync(legacyPath); } catch {} + console.log('[FirstRun] Migrated first-run.json to user data path'); + } + } catch (err) { + console.error('[FirstRun] Error migrating first-run.json:', err); + } +} + +function isFirstRun() { + migrateFirstRunFile(); + const firstRunPath = getOnboardingFilePath(); + try { + if (fs.existsSync(firstRunPath)) { + const data = JSON.parse(fs.readFileSync(firstRunPath, 'utf8')); + return !data.completed; + } + return true; // File doesn't exist, so it's first run + } catch (err) { + console.error('[FirstRun] Error checking first-run status:', err); + return true; // Assume first run on error + } +} + +/** + * Get first-run data + */ +function getFirstRunData() { + migrateFirstRunFile(); + const firstRunPath = getOnboardingFilePath(); + try { + if (fs.existsSync(firstRunPath)) { + return JSON.parse(fs.readFileSync(firstRunPath, 'utf8')); + } + return null; + } catch (err) { + console.error('[FirstRun] Error reading first-run data:', err); + return null; + } +} + +/** + * Complete first-run setup and save preferences + */ +async function completeFirstRun(preferences = {}) { + migrateFirstRunFile(); + const firstRunPath = getOnboardingFilePath(); + const data = { + completed: true, + skipped: preferences.skipped || false, + selectedThemeId: preferences.selectedTheme || 'default', + defaultBrowserAttempted: preferences.defaultBrowserSet || false, + defaultBrowserSet: preferences.defaultBrowserSet || false, + steamCloudOptIn: preferences.steamCloudOptIn || false, + completedAt: new Date().toISOString() + }; + + try { + if (portableData.isPortableMode()) { + await portableData.writeSecureFileAsync(firstRunPath, JSON.stringify(data, null, 2)); + } else { + await fs.promises.writeFile(firstRunPath, JSON.stringify(data, null, 2)); + } + console.log('[FirstRun] First-run setup completed:', data); + return true; + } catch (err) { + console.error('[FirstRun] Error saving first-run data:', err); + return false; + } +} + +/** + * Check if Nebula is set as the default browser + */ +function isDefaultBrowser() { + try { + return app.isDefaultProtocolClient('http') && app.isDefaultProtocolClient('https'); + } catch (err) { + console.error('[DefaultBrowser] Error checking default browser status:', err); + return false; + } +} + +/** + * Set Nebula as the default browser + */ +function setAsDefaultBrowser() { + try { + const httpResult = app.setAsDefaultProtocolClient('http'); + const httpsResult = app.setAsDefaultProtocolClient('https'); + const htmlResult = app.setAsDefaultProtocolClient('html'); + + console.log('[DefaultBrowser] Set as default:', { httpResult, httpsResult, htmlResult }); + return httpResult && httpsResult; + } catch (err) { + console.error('[DefaultBrowser] Error setting as default browser:', err); + return false; + } +} + +// ============================================================================= + // Initialize portable data paths BEFORE app.ready (must be done early) // This enables portable mode on all platforms (Windows, macOS, Linux) // Data is stored in 'user-data' folder within the application directory @@ -966,11 +1090,20 @@ function createWindow(startUrl, bigPictureMode = false) { }); // Load appropriate UI based on mode (Big Picture or Desktop) + // Check for first-run and load setup page if needed if (bigPictureMode) { win.loadFile('renderer/bigpicture.html'); win.setTitle('Nebula - Big Picture Mode'); } else { - win.loadFile('renderer/index.html'); + // Check if this is the first run (only for desktop mode) + const firstRun = isFirstRun(); + if (firstRun) { + console.log('[Startup] First run detected, loading setup page'); + win.loadFile('renderer/setup.html'); + win.setTitle('Welcome to Nebula'); + } else { + win.loadFile('renderer/index.html'); + } } perfMarks.loadFile_issued = performance.now(); @@ -1273,6 +1406,71 @@ ipcMain.handle('get-app-info', () => { }; }); +// --- First-Time Setup IPC handlers --- +ipcMain.handle('is-first-run', () => { + return isFirstRun(); +}); + +ipcMain.handle('get-first-run-data', () => { + return getFirstRunData(); +}); + +ipcMain.handle('complete-first-run', async (event, preferences) => { + try { + const success = await completeFirstRun(preferences); + return { success }; + } catch (err) { + console.error('[FirstRun] Error in IPC handler:', err); + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('get-all-themes', () => { + try { + const ThemeManager = require('./theme-manager.js'); + const manager = new ThemeManager(); + const themes = manager.getAllThemes(); + const defaultThemeCount = Object.keys(themes.default || {}).length; + const userThemeCount = Object.keys(themes.user || {}).length; + const downloadedThemeCount = Object.keys(themes.downloaded || {}).length; + console.log('[Themes] Loaded themes:', { + default: defaultThemeCount, + user: userThemeCount, + downloaded: downloadedThemeCount + }); + return themes; + } catch (err) { + console.error('[Themes] Error loading themes:', err); + return { default: { default: { name: 'Default', colors: {} } } }; + } +}); + +ipcMain.handle('apply-theme', async (event, themeId) => { + try { + // The theme will be applied in the renderer + // Here we just save the preference + console.log('[Themes] Theme selected:', themeId); + return { success: true }; + } catch (err) { + console.error('[Themes] Error applying theme:', err); + return { success: false, error: err.message }; + } +}); + +ipcMain.handle('is-default-browser', () => { + return isDefaultBrowser(); +}); + +ipcMain.handle('set-as-default-browser', () => { + try { + const result = setAsDefaultBrowser(); + return { success: result }; + } catch (err) { + console.error('[DefaultBrowser] Error in IPC handler:', err); + return { success: false, error: err.message }; + } +}); + // --- window control handlers (only registered once now) ipcMain.handle('window-minimize', event => { BrowserWindow.fromWebContents(event.sender).minimize(); diff --git a/preload.js b/preload.js index 7b6ea5e..0cefaba 100644 --- a/preload.js +++ b/preload.js @@ -505,6 +505,24 @@ contextBridge.exposeInMainWorld('updaterAPI', { onUpdateStatus: (handler) => ipcRenderer.on('update-status', (_e, payload) => handler(payload)) }); +// First-Time Setup API +contextBridge.exposeInMainWorld('api', { + // Check if this is the first run + isFirstRun: () => ipcRenderer.invoke('is-first-run'), + // Get all available themes + getAllThemes: () => ipcRenderer.invoke('get-all-themes'), + // Apply a theme + applyTheme: (themeId) => ipcRenderer.invoke('apply-theme', themeId), + // Check if Nebula is the default browser + isDefaultBrowser: () => ipcRenderer.invoke('is-default-browser'), + // Set Nebula as the default browser + setAsDefaultBrowser: () => ipcRenderer.invoke('set-as-default-browser'), + // Complete first-run setup + completeFirstRun: (data) => ipcRenderer.invoke('complete-first-run', data), + // Get first-run data + getFirstRunData: () => ipcRenderer.invoke('get-first-run-data') +}); + // ---------------------------------------- // Plugin renderer preloads // ---------------------------------------- diff --git a/renderer/setup.css b/renderer/setup.css new file mode 100644 index 0000000..f7764ee --- /dev/null +++ b/renderer/setup.css @@ -0,0 +1,644 @@ +/* Load InterVariable Font */ +@font-face { + font-family: 'InterVariable'; + src: url('../assets/images/fonts/InterVariable.ttf') format('truetype'); + font-weight: 100 900; + font-display: swap; +} + +/* CSS Custom Properties */ +:root { + --bg: #121418; + --dark-blue: #0B1C2B; + --dark-purple: #1B1035; + --primary: #7B2EFF; + --accent: #00C6FF; + --text: #E0E0E0; + --text-secondary: #A0A0A0; + --card-bg: rgba(255, 255, 255, 0.05); + --card-hover: rgba(255, 255, 255, 0.08); + --border: rgba(255, 255, 255, 0.1); + --success: #4CAF50; + --warning: #FF9800; + --gradient-primary: linear-gradient(135deg, var(--accent), var(--primary)); + --gradient-bg: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 100%); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +/* Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body, html { + margin: 0; + padding: 0; + height: 100%; + background: var(--gradient-bg); + color: var(--text); + font-family: 'InterVariable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + overflow: hidden; +} + +/* Setup Container */ +.setup-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + min-height: 100vh; + padding: 2rem; + overflow-y: auto; +} + +/* Progress Bar */ +.progress-bar { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-bottom: 3rem; + width: 100%; + max-width: 800px; +} + +.progress-step { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + opacity: 0.4; + transition: opacity 0.3s ease; +} + +.progress-step.active, +.progress-step.completed { + opacity: 1; +} + +.step-circle { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--card-bg); + border: 2px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 1.1rem; + transition: all 0.3s ease; +} + +.progress-step.active .step-circle { + background: var(--primary); + border-color: transparent; + box-shadow: 0 4px 12px rgba(123, 46, 255, 0.4); + transform: scale(1.1); +} + +.progress-step.completed .step-circle { + background: var(--success); + border-color: transparent; +} + +.step-label { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); +} + +.progress-step.active .step-label { + color: var(--text); + font-weight: 600; +} + +.progress-line { + flex: 1; + height: 2px; + background: var(--border); + margin: 0 0.5rem; + max-width: 100px; +} + +/* Setup Steps */ +.setup-step { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-width: 900px; + flex: 1; + animation: fadeIn 0.4s ease; + min-height: 0; +} + +.setup-step.active { + display: flex; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.step-content { + width: 100%; + text-align: center; + margin-bottom: 2rem; + flex: 1; + min-height: 0; +} + +.setup-title { + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 700; + letter-spacing: -0.5px; + margin-bottom: 0.75rem; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.setup-subtitle { + font-size: 1.1rem; + color: var(--text-secondary); + margin-bottom: 2rem; + font-weight: 400; +} + +/* Logo */ +.logo-container { + margin-bottom: 2rem; +} + +.setup-logo { + width: 120px; + height: 120px; + filter: drop-shadow(0 8px 24px rgba(123, 46, 255, 0.3)); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-10px); + } +} + +/* Feature Grid */ +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin: 2rem 0; +} + +.feature-item { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 16px; + padding: 1.5rem; + transition: all 0.3s ease; +} + +.feature-item:hover { + background: var(--card-hover); + border-color: var(--primary); + transform: translateY(-4px); + box-shadow: var(--shadow-md); +} + +.feature-icon { + font-size: 2.5rem; + margin-bottom: 0.75rem; +} + +.feature-item h3 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text); +} + +.feature-item p { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.5; +} + +/* Theme Grid */ +.theme-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; + margin: 1.5rem 0 2rem; + align-content: start; +} + +.theme-card { + background: var(--card-bg); + border: 2px solid var(--border); + border-radius: 16px; + padding: 1rem; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.theme-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); + border-color: var(--accent); +} + +.theme-card.selected { + border-color: var(--primary); + background: var(--card-hover); + box-shadow: 0 0 0 3px rgba(123, 46, 255, 0.2); +} + +.theme-card.selected::after { + content: '✓'; + position: absolute; + top: 12px; + right: 12px; + width: 32px; + height: 32px; + background: var(--gradient-primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + box-shadow: var(--shadow-sm); +} + +.theme-preview { + width: 100%; + height: 72px; + border-radius: 8px; + margin-bottom: 0.75rem; + display: flex; + gap: 4px; + padding: 8px; +} + +.theme-color { + flex: 1; + border-radius: 4px; + transition: transform 0.2s ease; +} + +.theme-card:hover .theme-color { + transform: scale(1.05); +} + +.theme-name { + font-size: 1rem; + font-weight: 600; + color: var(--text); + margin-bottom: 0.2rem; +} + +.theme-description { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* Default Browser Section */ +.default-browser-section { + max-width: 500px; + margin: 2rem auto; +} + +.default-browser-card { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2rem; + margin-bottom: 2rem; +} + +.browser-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.default-browser-card h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text); +} + +.default-browser-card p { + color: var(--text-secondary); + font-size: 1rem; + line-height: 1.6; +} + +.default-browser-status { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.status-icon { + font-size: 2rem; +} + +.status-text { + font-size: 0.95rem; + color: var(--text-secondary); +} + +.default-browser-status.checking .status-icon { + animation: spin 1s linear infinite; +} + +.default-browser-status.is-default { + border-color: var(--success); + background: rgba(76, 175, 80, 0.1); +} + +.default-browser-status.is-default .status-icon { + color: var(--success); +} + +.default-browser-status.is-default .status-text { + color: var(--success); +} + +.default-browser-status.not-default { + border-color: var(--warning); + background: rgba(255, 152, 0, 0.1); +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.default-browser-actions { + text-align: center; +} + +.help-text { + margin-top: 1rem; + font-size: 0.85rem; + color: var(--text-secondary); +} + +/* Success Icon */ +.success-icon { + width: 120px; + height: 120px; + margin: 0 auto 2rem; + background: var(--gradient-primary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 4rem; + font-weight: bold; + box-shadow: 0 8px 32px rgba(123, 46, 255, 0.4); + animation: scaleIn 0.5s ease; +} + +@keyframes scaleIn { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +/* Completion Summary */ +.completion-summary { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2rem; + margin: 2rem auto; + max-width: 500px; + text-align: left; +} + +.summary-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 0; + border-bottom: 1px solid var(--border); +} + +.summary-item:last-child { + border-bottom: none; +} + +.summary-icon { + font-size: 1.5rem; + width: 40px; + text-align: center; +} + +.summary-content { + flex: 1; +} + +.summary-label { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + +.summary-value { + font-size: 1rem; + color: var(--text); + font-weight: 500; +} + +/* Future Feature Teaser */ +.future-feature-teaser { + background: linear-gradient(135deg, rgba(123, 46, 255, 0.1), rgba(0, 198, 255, 0.1)); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2rem; + margin: 2rem auto; + max-width: 600px; +} + +.future-feature-teaser h3 { + font-size: 1.3rem; + margin-bottom: 1rem; + color: var(--text); +} + +.teaser-text { + display: flex; + align-items: center; + gap: 1rem; + font-size: 1rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.teaser-icon { + font-size: 2rem; +} + +/* Buttons */ +.step-actions { + display: flex; + gap: 1rem; + justify-content: center; + width: 100%; + max-width: 500px; + margin-top: auto; + padding-top: 1.5rem; + padding-bottom: 1rem; + position: sticky; + bottom: 0; + background: transparent; + backdrop-filter: none; +} + +.btn { + padding: 0.875rem 2rem; + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + font-family: 'InterVariable', sans-serif; + cursor: pointer; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-width: 140px; +} + +.btn-primary { + background: var(--primary); + color: white; + box-shadow: 0 4px 12px rgba(123, 46, 255, 0.3); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(123, 46, 255, 0.4); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-secondary { + background: var(--card-bg); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--card-hover); + border-color: var(--primary); +} + +.btn-large { + padding: 1.125rem 2.5rem; + font-size: 1.125rem; + min-width: 200px; +} + +.btn-icon { + font-size: 1.2rem; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .setup-container { + padding: 1rem; + } + + .progress-bar { + gap: 0.5rem; + margin-bottom: 2rem; + } + + .step-circle { + width: 40px; + height: 40px; + font-size: 0.95rem; + } + + .step-label { + font-size: 0.75rem; + } + + .progress-line { + max-width: 50px; + } + + .setup-title { + font-size: 2rem; + } + + .setup-subtitle { + font-size: 1rem; + } + + .feature-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .theme-grid { + grid-template-columns: 1fr; + margin-bottom: 1.5rem; + } + + .step-actions { + flex-direction: column-reverse; + width: 100%; + } + + .btn { + width: 100%; + } +} diff --git a/renderer/setup.html b/renderer/setup.html new file mode 100644 index 0000000..3545299 --- /dev/null +++ b/renderer/setup.html @@ -0,0 +1,141 @@ + + + + + + Welcome to Nebula + + + +
+ +
+
+
1
+
Welcome
+
+
+
+
2
+
Theme
+
+
+
+
3
+
Default Browser
+
+
+
+
4
+
Complete
+
+
+ + +
+
+
+ +
+

Welcome to Nebula

+

Let's personalize your browsing experience

+
+
+
🎨
+

Beautiful Themes

+

Choose from stunning themes or create your own

+
+
+
🚀
+

Lightning Fast

+

Built for speed and performance

+
+
+
🎮
+

Steam Deck Ready

+

Optimized for gaming handhelds

+
+
+
🔒
+

Privacy First

+

Your data stays yours

+
+
+
+
+ + +
+
+ + +
+
+

Choose Your Theme

+

Pick a color scheme that suits your style

+
+ +
+
+
+ + +
+
+ + +
+
+

Set as Default Browser

+

Make Nebula your go-to browser for all links

+
+
+
🌐
+

Quick Access

+

Open all web links automatically with Nebula

+
+
+
âŗ
+

Checking default browser status...

+
+
+ +

You can always change this later in settings

+
+
+
+
+ + +
+
+ + +
+
+
✓
+

All Set!

+

You're ready to explore the web with Nebula

+
+ +
+
+

Coming Soon: Steam Cloud Sync

+

+ â˜ī¸ + In Phase 2, you'll be able to sync your bookmarks, settings, and themes across devices using Steam Cloud. +

+
+
+
+ +
+
+
+ + + + diff --git a/renderer/setup.js b/renderer/setup.js new file mode 100644 index 0000000..a3d5e85 --- /dev/null +++ b/renderer/setup.js @@ -0,0 +1,538 @@ +/** + * First-Time Setup Script for Nebula Browser + * Handles theme selection, default browser setup, and first-run completion + */ + +// State management +const setupState = { + currentStep: 1, + selectedTheme: 'default', + defaultBrowserSet: false, + skipped: false, + themes: [] +}; + +// Initialize setup when DOM is ready +document.addEventListener('DOMContentLoaded', async () => { + console.log('[Setup] Initializing first-time setup...'); + + // Load available themes + await loadThemes(); + + // Initialize button handlers + initializeButtons(); + + // Check default browser status + checkDefaultBrowserStatus(); +}); + +/** + * Load available themes from main process + */ +async function loadThemes() { + try { + const themes = await window.api.getAllThemes(); + console.log('[Setup] Loaded themes:', themes); + setupState.themes = themes; + + // Render theme grid + renderThemeGrid(themes); + } catch (error) { + console.error('[Setup] Error loading themes:', error); + // Fallback to a default theme + setupState.themes = { + default: { + default: { name: 'Default', description: 'Classic Nebula theme', colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF' } } + } + }; + renderThemeGrid(setupState.themes); + } +} + +/** + * Render theme selection grid + */ +function renderThemeGrid(themes) { + const themeGrid = document.getElementById('theme-grid'); + if (!themeGrid) return; + + themeGrid.innerHTML = ''; + + // Convert themes object to array + let themeArray = []; + + if (Array.isArray(themes)) { + // Already an array + themeArray = themes; + } else if (themes.default) { + // Has default property, extract themes from it + themeArray = Object.entries(themes.default).map(([id, data]) => ({ + id, + name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '), + description: data.description || 'A beautiful color scheme', + colors: data.colors || {} + })); + } else { + // Direct object of themes + themeArray = Object.entries(themes).map(([id, data]) => ({ + id, + name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '), + description: data.description || 'A beautiful color scheme', + colors: data.colors || {} + })); + } + + console.log('[Setup] Rendering', themeArray.length, 'themes'); + + // If no themes found, add a default one + if (themeArray.length === 0) { + themeArray = [{ + id: 'default', + name: 'Default', + description: 'Classic Nebula theme', + colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' } + }]; + } + + themeArray.forEach(theme => { + const themeCard = createThemeCard(theme); + themeGrid.appendChild(themeCard); + }); + + // Select default theme + const defaultCard = themeGrid.querySelector('[data-theme-id="default"]'); + if (defaultCard) { + defaultCard.classList.add('selected'); + const defaultTheme = getThemeById('default'); + if (defaultTheme) { + applyThemeToSetupPage(defaultTheme, 'default'); + } + } +} + +/** + * Get a theme by id from loaded theme sets + */ +function getThemeById(themeId) { + const themes = setupState.themes || {}; + if (themes.default && themes.default[themeId]) return themes.default[themeId]; + if (themes.user && themes.user[themeId]) return themes.user[themeId]; + if (themes.downloaded && themes.downloaded[themeId]) return themes.downloaded[themeId]; + return null; +} + +/** + * Apply theme to the setup page UI and persist selection + */ +function applyThemeToSetupPage(theme, themeId = null) { + if (!theme || !theme.colors) return; + const colors = theme.colors; + const root = document.documentElement; + + const setVar = (cssVar, value, fallback) => { + const val = value || fallback; + if (val) root.style.setProperty(cssVar, val); + }; + + setVar('--bg', colors.bg, '#121418'); + setVar('--dark-blue', colors.darkBlue, '#0B1C2B'); + setVar('--dark-purple', colors.darkPurple, '#1B1035'); + setVar('--primary', colors.primary, '#7B2EFF'); + setVar('--accent', colors.accent, '#00C6FF'); + setVar('--text', colors.text, '#E0E0E0'); + + if (theme.gradient) { + document.body.style.background = theme.gradient; + } else if (colors.bg) { + document.body.style.background = colors.bg; + } + + // Persist for main UI to pick up on first load + try { + localStorage.setItem('currentTheme', JSON.stringify(theme)); + if (themeId) localStorage.setItem('activeThemeName', themeId); + } catch (err) { + console.warn('[Setup] Failed to persist theme:', err); + } +} + +/** + * Create a theme card element + */ +function createThemeCard(theme) { + const card = document.createElement('div'); + card.className = 'theme-card'; + card.dataset.themeId = theme.id; + + // Create color preview + const preview = document.createElement('div'); + preview.className = 'theme-preview'; + + const colors = theme.colors || {}; + + // Get color values, trying multiple property naming conventions + const getColor = (keys, fallback) => { + for (const key of keys) { + if (colors[key]) return colors[key]; + } + return fallback; + }; + + const previewColors = [ + getColor(['bg', '--bg', 'background'], '#121418'), + getColor(['primary', '--primary'], '#7B2EFF'), + getColor(['accent', '--accent'], '#00C6FF'), + getColor(['text', '--text'], '#E0E0E0') + ]; + + previewColors.forEach(color => { + const colorDiv = document.createElement('div'); + colorDiv.className = 'theme-color'; + colorDiv.style.backgroundColor = color; + preview.appendChild(colorDiv); + }); + + // Create theme info + const name = document.createElement('div'); + name.className = 'theme-name'; + name.textContent = theme.name || theme.id; + + const description = document.createElement('div'); + description.className = 'theme-description'; + description.textContent = theme.description || 'A beautiful color scheme'; + + // Assemble card + card.appendChild(preview); + card.appendChild(name); + card.appendChild(description); + + // Add click handler + card.addEventListener('click', () => selectTheme(theme.id, card)); + + return card; +} + +/** + * Select a theme + */ +function selectTheme(themeId, cardElement) { + // Update state + setupState.selectedTheme = themeId; + + // Update UI + document.querySelectorAll('.theme-card').forEach(card => { + card.classList.remove('selected'); + }); + cardElement.classList.add('selected'); + + const theme = getThemeById(themeId); + if (theme) { + applyThemeToSetupPage(theme, themeId); + } + + console.log('[Setup] Selected theme:', themeId); +} + +/** + * Check if Nebula is the default browser + */ +async function checkDefaultBrowserStatus() { + const statusEl = document.getElementById('default-status'); + if (!statusEl) return; + + statusEl.classList.add('checking'); + + try { + const isDefault = await window.api.isDefaultBrowser(); + + statusEl.classList.remove('checking'); + + if (isDefault) { + statusEl.classList.add('is-default'); + statusEl.innerHTML = ` +
✓
+

Nebula is already your default browser

+ `; + setupState.defaultBrowserSet = true; + + // Update button + const setDefaultBtn = document.getElementById('btn-set-default'); + if (setDefaultBtn) { + setDefaultBtn.textContent = '✓ Already Default'; + setDefaultBtn.disabled = true; + } + } else { + statusEl.classList.add('not-default'); + statusEl.innerHTML = ` +
â„šī¸
+

Nebula is not your default browser

+ `; + } + } catch (error) { + console.error('[Setup] Error checking default browser status:', error); + statusEl.classList.remove('checking'); + statusEl.innerHTML = ` +
âš ī¸
+

Unable to check default browser status

+ `; + } +} + +/** + * Set Nebula as default browser + */ +async function setDefaultBrowser() { + const btn = document.getElementById('btn-set-default'); + const statusEl = document.getElementById('default-status'); + + if (btn) { + btn.disabled = true; + btn.innerHTML = 'âŗ Setting...'; + } + + try { + const result = await window.api.setAsDefaultBrowser(); + + if (result.success) { + setupState.defaultBrowserSet = true; + + if (statusEl) { + statusEl.classList.remove('not-default'); + statusEl.classList.add('is-default'); + statusEl.innerHTML = ` +
✓
+

Nebula is now your default browser!

+ `; + } + + if (btn) { + btn.innerHTML = '✓ Set Successfully'; + } + + // Auto-advance after a brief delay + setTimeout(() => goToStep(4), 1500); + } else { + throw new Error(result.error || 'Failed to set default browser'); + } + } catch (error) { + console.error('[Setup] Error setting default browser:', error); + + if (statusEl) { + statusEl.innerHTML = ` +
âš ī¸
+

Failed to set default browser. You can try again from settings.

+ `; + } + + if (btn) { + btn.disabled = false; + btn.innerHTML = 'â†ģ Try Again'; + } + } +} + +/** + * Navigate to a specific step + */ +function goToStep(stepNumber) { + // Hide current step + document.querySelectorAll('.setup-step').forEach(step => { + step.classList.remove('active'); + }); + + // Show target step + const targetStep = document.querySelector(`.setup-step[data-step="${stepNumber}"]`); + if (targetStep) { + targetStep.classList.add('active'); + } + + // Update progress bar + document.querySelectorAll('.progress-step').forEach((step, index) => { + const stepNum = index + 1; + if (stepNum < stepNumber) { + step.classList.add('completed'); + step.classList.remove('active'); + } else if (stepNum === stepNumber) { + step.classList.add('active'); + step.classList.remove('completed'); + } else { + step.classList.remove('active', 'completed'); + } + }); + + setupState.currentStep = stepNumber; + + // Special handling for completion step + if (stepNumber === 4) { + renderCompletionSummary(); + } + + console.log('[Setup] Navigated to step:', stepNumber); +} + +/** + * Render completion summary + */ +function renderCompletionSummary() { + const summaryEl = document.getElementById('completion-summary'); + if (!summaryEl) return; + + const selectedThemeName = setupState.themes.default?.[setupState.selectedTheme]?.name || + setupState.selectedTheme.charAt(0).toUpperCase() + setupState.selectedTheme.slice(1); + + summaryEl.innerHTML = ` +
+
🎨
+
+
Selected Theme
+
${selectedThemeName}
+
+
+
+
🌐
+
+
Default Browser
+
${setupState.defaultBrowserSet ? 'Set as Default' : 'Not Set'}
+
+
+
+
â˜ī¸
+
+
Steam Cloud Sync
+
Coming in Phase 2
+
+
+ `; +} + +/** + * Complete setup and save preferences + */ +async function completeSetup() { + console.log('[Setup] Completing first-time setup...', setupState); + + try { + // Apply selected theme + await window.api.applyTheme(setupState.selectedTheme); + + // Save first-run completion + await window.api.completeFirstRun({ + selectedTheme: setupState.selectedTheme, + defaultBrowserSet: setupState.defaultBrowserSet, + skipped: setupState.skipped + }); + + console.log('[Setup] First-time setup completed successfully'); + + // Navigate to main browser interface (index.html has tabs and URL bar) + window.location.href = 'index.html'; + } catch (error) { + console.error('[Setup] Error completing setup:', error); + alert('There was an error saving your preferences. Please try again.'); + } +} + +/** + * Skip setup and use defaults + */ +async function skipSetup() { + setupState.skipped = true; + + try { + // Save that first-run was completed (even if skipped) + await window.api.completeFirstRun({ + selectedTheme: 'default', + defaultBrowserSet: false, + skipped: true + }); + + console.log('[Setup] Setup skipped, using defaults'); + + // Navigate to main browser interface (index.html has tabs and URL bar) + window.location.href = 'index.html'; + } catch (error) { + console.error('[Setup] Error skipping setup:', error); + window.location.href = 'index.html'; + } +} + +/** + * Initialize button event handlers + */ +function initializeButtons() { + // Step 1: Welcome + const btnStart = document.getElementById('btn-start'); + const btnSkipAll = document.getElementById('btn-skip-all'); + + if (btnStart) { + btnStart.addEventListener('click', () => goToStep(2)); + } + + if (btnSkipAll) { + btnSkipAll.addEventListener('click', skipSetup); + } + + // Step 2: Theme Selection + const btnBack2 = document.getElementById('btn-back-2'); + const btnNext2 = document.getElementById('btn-next-2'); + + if (btnBack2) { + btnBack2.addEventListener('click', () => goToStep(1)); + } + + if (btnNext2) { + btnNext2.addEventListener('click', () => goToStep(3)); + } + + // Step 3: Default Browser + const btnBack3 = document.getElementById('btn-back-3'); + const btnSkip3 = document.getElementById('btn-skip-3'); + const btnSetDefault = document.getElementById('btn-set-default'); + + if (btnBack3) { + btnBack3.addEventListener('click', () => goToStep(2)); + } + + if (btnSkip3) { + btnSkip3.addEventListener('click', () => goToStep(4)); + } + + if (btnSetDefault) { + btnSetDefault.addEventListener('click', setDefaultBrowser); + } + + // Step 4: Complete + const btnFinish = document.getElementById('btn-finish'); + + if (btnFinish) { + btnFinish.addEventListener('click', completeSetup); + } +} + +// Keyboard navigation +document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const currentStep = setupState.currentStep; + + switch (currentStep) { + case 1: + goToStep(2); + break; + case 2: + goToStep(3); + break; + case 3: + if (!setupState.defaultBrowserSet) { + setDefaultBrowser(); + } else { + goToStep(4); + } + break; + case 4: + completeSetup(); + break; + } + } else if (e.key === 'Escape' && setupState.currentStep > 1) { + goToStep(setupState.currentStep - 1); + } +}); diff --git a/theme-manager.js b/theme-manager.js index d0a07b2..7885e27 100644 --- a/theme-manager.js +++ b/theme-manager.js @@ -8,7 +8,7 @@ const path = require('path'); class ThemeManager { constructor() { - this.themesDir = path.join(__dirname, '..', 'themes'); + this.themesDir = path.join(__dirname, 'themes'); this.userThemesDir = path.join(this.themesDir, 'user'); this.downloadedThemesDir = path.join(this.themesDir, 'downloaded');