Add first-time setup onboarding flow
Introduces a multi-step onboarding process for first-time users, including theme selection and default browser setup. Adds setup.html, setup.js, and setup.css for the new UI, updates main.js and preload.js to support onboarding logic and IPC handlers, and adjusts theme-manager.js for correct theme directory resolution.
This commit is contained in:
@@ -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,12 +1090,21 @@ 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 {
|
||||
// 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();
|
||||
|
||||
// if caller passed in a URL, forward it to the renderer after load
|
||||
@@ -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();
|
||||
|
||||
+18
@@ -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
|
||||
// ----------------------------------------
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Nebula</title>
|
||||
<link rel="stylesheet" href="setup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="setup-container">
|
||||
<!-- Progress indicator -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-step active" data-step="1">
|
||||
<div class="step-circle">1</div>
|
||||
<div class="step-label">Welcome</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="2">
|
||||
<div class="step-circle">2</div>
|
||||
<div class="step-label">Theme</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="3">
|
||||
<div class="step-circle">3</div>
|
||||
<div class="step-label">Default Browser</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="4">
|
||||
<div class="step-circle">4</div>
|
||||
<div class="step-label">Complete</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div class="setup-step active" data-step="1">
|
||||
<div class="step-content">
|
||||
<div class="logo-container">
|
||||
<img src="../assets/images/Logos/Nebula-Icon.png" alt="Nebula Logo" class="setup-logo">
|
||||
</div>
|
||||
<h1 class="setup-title">Welcome to Nebula</h1>
|
||||
<p class="setup-subtitle">Let's personalize your browsing experience</p>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>Beautiful Themes</h3>
|
||||
<p>Choose from stunning themes or create your own</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🚀</div>
|
||||
<h3>Lightning Fast</h3>
|
||||
<p>Built for speed and performance</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🎮</div>
|
||||
<h3>Steam Deck Ready</h3>
|
||||
<p>Optimized for gaming handhelds</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>Privacy First</h3>
|
||||
<p>Your data stays yours</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-primary" id="btn-start">Get Started</button>
|
||||
<button class="btn btn-secondary" id="btn-skip-all">Skip Setup</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Theme Selection -->
|
||||
<div class="setup-step" data-step="2">
|
||||
<div class="step-content">
|
||||
<h1 class="setup-title">Choose Your Theme</h1>
|
||||
<p class="setup-subtitle">Pick a color scheme that suits your style</p>
|
||||
<div class="theme-grid" id="theme-grid">
|
||||
<!-- Themes will be dynamically loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" id="btn-back-2">Back</button>
|
||||
<button class="btn btn-primary" id="btn-next-2">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Default Browser -->
|
||||
<div class="setup-step" data-step="3">
|
||||
<div class="step-content">
|
||||
<h1 class="setup-title">Set as Default Browser</h1>
|
||||
<p class="setup-subtitle">Make Nebula your go-to browser for all links</p>
|
||||
<div class="default-browser-section">
|
||||
<div class="default-browser-card">
|
||||
<div class="browser-icon">🌐</div>
|
||||
<h3>Quick Access</h3>
|
||||
<p>Open all web links automatically with Nebula</p>
|
||||
</div>
|
||||
<div class="default-browser-status" id="default-status">
|
||||
<div class="status-icon">⏳</div>
|
||||
<p class="status-text">Checking default browser status...</p>
|
||||
</div>
|
||||
<div class="default-browser-actions">
|
||||
<button class="btn btn-large btn-primary" id="btn-set-default">
|
||||
<span class="btn-icon">✓</span>
|
||||
Set as Default Browser
|
||||
</button>
|
||||
<p class="help-text">You can always change this later in settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" id="btn-back-3">Back</button>
|
||||
<button class="btn btn-primary" id="btn-skip-3">Skip for Now</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Complete (Future: Steam Cloud) -->
|
||||
<div class="setup-step" data-step="4">
|
||||
<div class="step-content">
|
||||
<div class="success-icon">✓</div>
|
||||
<h1 class="setup-title">All Set!</h1>
|
||||
<p class="setup-subtitle">You're ready to explore the web with Nebula</p>
|
||||
<div class="completion-summary" id="completion-summary">
|
||||
<!-- Summary will be populated dynamically -->
|
||||
</div>
|
||||
<div class="future-feature-teaser">
|
||||
<h3>Coming Soon: Steam Cloud Sync</h3>
|
||||
<p class="teaser-text">
|
||||
<span class="teaser-icon">☁️</span>
|
||||
In Phase 2, you'll be able to sync your bookmarks, settings, and themes across devices using Steam Cloud.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-primary btn-large" id="btn-finish">Start Browsing</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="setup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = `
|
||||
<div class="status-icon">✓</div>
|
||||
<p class="status-text">Nebula is already your default browser</p>
|
||||
`;
|
||||
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 = `
|
||||
<div class="status-icon">ℹ️</div>
|
||||
<p class="status-text">Nebula is not your default browser</p>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error checking default browser status:', error);
|
||||
statusEl.classList.remove('checking');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">⚠️</div>
|
||||
<p class="status-text">Unable to check default browser status</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = '<span class="btn-icon">⏳</span> 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 = `
|
||||
<div class="status-icon">✓</div>
|
||||
<p class="status-text">Nebula is now your default browser!</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.innerHTML = '<span class="btn-icon">✓</span> 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 = `
|
||||
<div class="status-icon">⚠️</div>
|
||||
<p class="status-text">Failed to set default browser. You can try again from settings.</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="btn-icon">↻</span> 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 = `
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🎨</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">Selected Theme</div>
|
||||
<div class="summary-value">${selectedThemeName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🌐</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">Default Browser</div>
|
||||
<div class="summary-value">${setupState.defaultBrowserSet ? 'Set as Default' : 'Not Set'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">☁️</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">Steam Cloud Sync</div>
|
||||
<div class="summary-value">Coming in Phase 2</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user