Add Nebula Browser app, UI and assets

Add initial Nebula Browser project skeleton: CMakeLists to configure and link CEF (including post-build steps to copy runtime and UI files), a Windows CEF-based entry (app/main.cpp) that initializes CEF and loads the bundled UI, and a full ui/ and assets/ tree (HTML, CSS, JS, fonts, icons, and branding images). Update .gitignore to ignore build/out, thirdparty/cef, IDE and common OS artifacts.
This commit is contained in:
Andrew Zambazos
2026-05-13 22:17:58 +12:00
parent 79565f2ef3
commit 207a849f06
52 changed files with 13906 additions and 109 deletions
+3037
View File
File diff suppressed because it is too large Load Diff
+849
View File
@@ -0,0 +1,849 @@
/**
* Browser Customization System
* Allows users to customize themes, colors, and layouts non-destructively
*/
class BrowserCustomizer {
constructor() {
this.defaultTheme = {
name: 'Default',
colors: {
bg: '#121418',
darkBlue: '#0B1C2B',
darkPurple: '#1B1035',
primary: '#7B2EFF',
accent: '#00C6FF',
text: '#E0E0E0',
urlBarBg: '#1C2030',
urlBarText: '#E0E0E0',
urlBarBorder: '#3E4652',
tabBg: '#161925',
tabText: '#A4A7B3',
tabActive: '#1C2030',
tabActiveText: '#E0E0E0',
tabBorder: '#2B3040'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #121418 0%, #1B1035 100%)'
};
this.predefinedThemes = {
default: this.defaultTheme,
ocean: {
name: 'Ocean',
colors: {
bg: '#1a365d',
darkBlue: '#2a4365',
darkPurple: '#2c5282',
primary: '#3182ce',
accent: '#00d9ff',
text: '#e2e8f0',
urlBarBg: '#2d5282',
urlBarText: '#e2e8f0',
urlBarBorder: '#1e3a5f',
tabBg: '#2a4365',
tabText: '#cbd5e0',
tabActive: '#2d5282',
tabActiveText: '#e2e8f0',
tabBorder: '#1a365d'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #1a365d 0%, #2c5282 100%)'
},
forest: {
name: 'Forest',
colors: {
bg: '#1a202c',
darkBlue: '#2d3748',
darkPurple: '#4a5568',
primary: '#68d391',
accent: '#9ae6b4',
text: '#f7fafc',
urlBarBg: '#2d3748',
urlBarText: '#f7fafc',
urlBarBorder: '#4a5568',
tabBg: '#2d3748',
tabText: '#cbd5e0',
tabActive: '#4a5568',
tabActiveText: '#f7fafc',
tabBorder: '#1a202c'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #1a202c 0%, #2d3748 100%)'
},
sunset: {
name: 'Sunset',
colors: {
bg: '#744210',
darkBlue: '#975a16',
darkPurple: '#c05621',
primary: '#ed8936',
accent: '#fbb040',
text: '#fffaf0',
urlBarBg: '#975a16',
urlBarText: '#fffaf0',
urlBarBorder: '#c05621',
tabBg: '#975a16',
tabText: '#fde4b6',
tabActive: '#c05621',
tabActiveText: '#fffaf0',
tabBorder: '#744210'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #744210 0%, #c05621 100%)'
},
cyberpunk: {
name: 'Cyberpunk Neon',
colors: {
bg: '#0a0a0a',
darkBlue: '#1a0520',
darkPurple: '#2a0a3a',
primary: '#ff0080',
accent: '#00ffff',
text: '#ffffff',
urlBarBg: '#1a0520',
urlBarText: '#ffffff',
urlBarBorder: '#ff0080',
tabBg: '#1a0520',
tabText: '#00ffff',
tabActive: '#2a0a3a',
tabActiveText: '#ff0080',
tabBorder: '#ff0080'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #0a0a0a 0%, #2a0a3a 50%, #1a0520 100%)'
},
'midnight-rose': {
name: 'Midnight Rose',
colors: {
bg: '#1c1820',
darkBlue: '#2d2433',
darkPurple: '#3d3046',
primary: '#d4af37',
accent: '#ffd700',
text: '#f5f5dc',
urlBarBg: '#3d3046',
urlBarText: '#f5f5dc',
urlBarBorder: '#d4af37',
tabBg: '#2d2433',
tabText: '#d4af37',
tabActive: '#3d3046',
tabActiveText: '#ffd700',
tabBorder: '#1c1820'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #1c1820 0%, #3d3046 100%)'
},
'arctic-ice': {
name: 'Arctic Ice',
colors: {
bg: '#f0f8ff',
darkBlue: '#e6f3ff',
darkPurple: '#d1e7ff',
primary: '#4169e1',
accent: '#87ceeb',
text: '#2f4f4f',
urlBarBg: '#e6f3ff',
urlBarText: '#2f4f4f',
urlBarBorder: '#4169e1',
tabBg: '#e6f3ff',
tabText: '#4169e1',
tabActive: '#d1e7ff',
tabActiveText: '#2f4f4f',
tabBorder: '#f0f8ff'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #f0f8ff 0%, #d1e7ff 100%)'
},
'cherry-blossom': {
name: 'Cherry Blossom',
colors: {
bg: '#fff5f8',
darkBlue: '#ffe4e8',
darkPurple: '#ffd4db',
primary: '#ff69b4',
accent: '#ffb6c1',
text: '#8b4513',
urlBarBg: '#ffe4e8',
urlBarText: '#8b4513',
urlBarBorder: '#ff69b4',
tabBg: '#ffe4e8',
tabText: '#ff69b4',
tabActive: '#ffd4db',
tabActiveText: '#8b4513',
tabBorder: '#fff5f8'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #fff5f8 0%, #ffd4db 100%)'
},
'cosmic-purple': {
name: 'Cosmic Purple',
colors: {
bg: '#0f0524',
darkBlue: '#1a0b3d',
darkPurple: '#2d1b69',
primary: '#8a2be2',
accent: '#da70d6',
text: '#e6e6fa',
urlBarBg: '#1a0b3d',
urlBarText: '#e6e6fa',
urlBarBorder: '#8a2be2',
tabBg: '#1a0b3d',
tabText: '#da70d6',
tabActive: '#2d1b69',
tabActiveText: '#e6e6fa',
tabBorder: '#0f0524'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #0f0524 0%, #2d1b69 50%, #4b0082 100%)'
},
'emerald-dream': {
name: 'Emerald Dream',
colors: {
bg: '#0d2818',
darkBlue: '#1a3a2e',
darkPurple: '#2d5a44',
primary: '#50c878',
accent: '#98fb98',
text: '#f0fff0',
urlBarBg: '#1a3a2e',
urlBarText: '#f0fff0',
urlBarBorder: '#50c878',
tabBg: '#1a3a2e',
tabText: '#98fb98',
tabActive: '#2d5a44',
tabActiveText: '#f0fff0',
tabBorder: '#0d2818'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #0d2818 0%, #2d5a44 100%)'
},
'mocha-coffee': {
name: 'Mocha Coffee',
colors: {
bg: '#3c2414',
darkBlue: '#4a2c1a',
darkPurple: '#5d3a26',
primary: '#d2691e',
accent: '#daa520',
text: '#faf0e6',
urlBarBg: '#4a2c1a',
urlBarText: '#faf0e6',
urlBarBorder: '#d2691e',
tabBg: '#4a2c1a',
tabText: '#daa520',
tabActive: '#5d3a26',
tabActiveText: '#faf0e6',
tabBorder: '#3c2414'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #3c2414 0%, #5d3a26 100%)'
},
'lavender-fields': {
name: 'Lavender Fields',
colors: {
bg: '#f8f4ff',
darkBlue: '#ede4ff',
darkPurple: '#e6d8ff',
primary: '#9370db',
accent: '#dda0dd',
text: '#4b0082',
urlBarBg: '#ede4ff',
urlBarText: '#4b0082',
urlBarBorder: '#9370db',
tabBg: '#ede4ff',
tabText: '#9370db',
tabActive: '#e6d8ff',
tabActiveText: '#4b0082',
tabBorder: '#f8f4ff'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #f8f4ff 0%, #e6d8ff 100%)'
}
};
this.currentTheme = this.loadTheme();
this.activeThemeName = this.loadActiveThemeName();
this.init();
}
init() {
this.setupEventListeners();
this.loadCurrentTheme();
this.restoreActiveThemeButton();
this.updatePreview();
this.updateCustomThemeButton();
}
setupEventListeners() {
// Theme preset buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const themeName = e.currentTarget.dataset.theme;
this.applyPredefinedTheme(themeName);
});
});
// Color inputs
const colorInputs = ['bg-color', 'gradient-color', 'accent-color', 'secondary-color', 'text-color'];
colorInputs.forEach(inputId => {
const input = document.getElementById(inputId);
if (input) {
input.addEventListener('input', (e) => {
this.updateColorFromInput(inputId, e.target.value);
});
}
});
// Layout options
document.querySelectorAll('input[name="layout"]').forEach(input => {
input.addEventListener('change', (e) => {
this.currentTheme.layout = e.target.value;
// Clear active theme name since this is now a custom theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
// Remove active class from all theme buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
this.updateCustomThemeButton();
});
});
// Logo options
const showLogoInput = document.getElementById('show-logo');
if (showLogoInput) {
showLogoInput.addEventListener('change', (e) => {
this.currentTheme.showLogo = e.target.checked;
// Clear active theme name since this is now a custom theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
// Remove active class from all theme buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
this.updateCustomThemeButton();
});
}
const customTitleInput = document.getElementById('custom-title');
if (customTitleInput) {
customTitleInput.addEventListener('input', (e) => {
this.currentTheme.customTitle = e.target.value || 'Nebula Browser';
// Clear active theme name since this is now a custom theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
// Remove active class from all theme buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
this.updateCustomThemeButton();
});
}
// Theme management buttons
this.setupThemeManagementButtons();
}
setupThemeManagementButtons() {
const saveBtn = document.getElementById('save-custom-theme');
const exportBtn = document.getElementById('export-theme');
const importBtn = document.getElementById('import-theme');
const resetBtn = document.getElementById('reset-to-default');
const fileInput = document.getElementById('theme-file-input');
if (saveBtn) {
saveBtn.addEventListener('click', () => this.saveCustomTheme());
}
if (exportBtn) {
exportBtn.addEventListener('click', () => this.exportTheme());
}
if (importBtn) {
importBtn.addEventListener('click', () => fileInput.click());
}
if (fileInput) {
fileInput.addEventListener('change', (e) => this.importTheme(e));
}
if (resetBtn) {
resetBtn.addEventListener('click', () => this.resetToDefault());
}
}
updateColorFromInput(inputId, color) {
const colorMap = {
'bg-color': 'bg',
'gradient-color': 'darkPurple',
'accent-color': 'primary',
'secondary-color': 'accent',
'text-color': 'text'
};
const colorKey = colorMap[inputId];
if (colorKey) {
this.currentTheme.colors[colorKey] = color;
// Update gradient for background or gradient changes
if (colorKey === 'bg' || colorKey === 'darkPurple') {
this.currentTheme.gradient = `linear-gradient(145deg, ${this.currentTheme.colors.bg} 0%, ${this.currentTheme.colors.darkPurple} 100%)`;
}
// Clear active theme name since this is now a custom theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
// Remove active class from all theme buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
this.updateCustomThemeButton();
}
}
applyPredefinedTheme(themeName) {
if (themeName === 'custom') {
// For custom theme, just activate the button but don't change the current theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
this.updateThemeButtons('custom');
this.updateCustomThemeButton();
} else if (this.predefinedThemes[themeName]) {
this.currentTheme = { ...this.predefinedThemes[themeName] };
this.activeThemeName = themeName;
this.saveTheme();
this.saveActiveThemeName(themeName);
this.loadCurrentTheme();
this.updatePreview();
this.applyThemeToCurrentPage();
this.applyThemeToPages();
this.updateThemeButtons(themeName);
this.updateCustomThemeButton();
}
}
updateThemeButtons(activeTheme) {
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.theme === activeTheme) {
btn.classList.add('active');
}
});
}
updateCustomThemeButton() {
const customBtn = document.getElementById('theme-custom');
if (!customBtn) return;
// Check if current theme matches any predefined theme
const matchingTheme = this.detectMatchingPredefinedTheme();
const isCustomTheme = !matchingTheme;
if (isCustomTheme) {
customBtn.style.display = 'flex';
// Update the preview to show current colors
const preview = customBtn.querySelector('.theme-preview');
if (preview && this.currentTheme) {
preview.style.background = this.currentTheme.gradient ||
`linear-gradient(145deg, ${this.currentTheme.colors.bg}, ${this.currentTheme.colors.darkPurple})`;
}
// Set active theme name to custom if it's not already set to a predefined theme
if (this.activeThemeName !== 'custom') {
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
}
} else {
customBtn.style.display = 'none';
// If we found a matching predefined theme, update activeThemeName if it was set to custom
if (this.activeThemeName === 'custom') {
this.activeThemeName = matchingTheme;
this.saveActiveThemeName(matchingTheme);
}
}
}
loadCurrentTheme() {
// Update color inputs
document.getElementById('bg-color').value = this.currentTheme.colors.bg;
document.getElementById('gradient-color').value = this.currentTheme.colors.darkPurple;
document.getElementById('accent-color').value = this.currentTheme.colors.primary;
document.getElementById('secondary-color').value = this.currentTheme.colors.accent;
document.getElementById('text-color').value = this.currentTheme.colors.text;
// Update layout radio
const layoutInput = document.querySelector(`input[name="layout"][value="${this.currentTheme.layout}"]`);
if (layoutInput) layoutInput.checked = true;
// Update logo options
document.getElementById('show-logo').checked = this.currentTheme.showLogo;
document.getElementById('custom-title').value = this.currentTheme.customTitle;
}
updatePreview() {
const preview = document.getElementById('preview-container');
const previewHome = preview.querySelector('.preview-home');
const previewLogo = preview.querySelector('.preview-logo');
const previewText = preview.querySelector('.preview-text');
// Apply colors to preview
previewHome.style.background = this.currentTheme.gradient;
// Handle logo visibility
if (this.currentTheme.showLogo) {
previewLogo.style.display = 'block';
previewLogo.style.color = this.currentTheme.colors.primary;
previewLogo.textContent = '🌌';
} else {
previewLogo.style.display = 'none';
}
// Always show preview text with custom title
if (previewText) {
previewText.style.color = this.currentTheme.colors.primary;
previewText.textContent = this.currentTheme.customTitle;
}
// Update CSS custom properties for live preview
this.applyThemeToCurrentPage();
}
applyThemeToCurrentPage() {
const root = document.documentElement;
root.style.setProperty('--bg', this.currentTheme.colors.bg);
root.style.setProperty('--dark-blue', this.currentTheme.colors.darkBlue);
root.style.setProperty('--dark-purple', this.currentTheme.colors.darkPurple);
root.style.setProperty('--primary', this.currentTheme.colors.primary);
root.style.setProperty('--accent', this.currentTheme.colors.accent);
root.style.setProperty('--text', this.currentTheme.colors.text);
root.style.setProperty('--url-bar-bg', this.currentTheme.colors.urlBarBg);
root.style.setProperty('--url-bar-text', this.currentTheme.colors.urlBarText);
root.style.setProperty('--url-bar-border', this.currentTheme.colors.urlBarBorder);
root.style.setProperty('--tab-bg', this.currentTheme.colors.tabBg);
root.style.setProperty('--tab-text', this.currentTheme.colors.tabText);
root.style.setProperty('--tab-active', this.currentTheme.colors.tabActive);
root.style.setProperty('--tab-active-text', this.currentTheme.colors.tabActiveText);
root.style.setProperty('--tab-border', this.currentTheme.colors.tabBorder);
// Apply gradient to body if it exists
const body = document.body;
if (body && this.currentTheme.gradient) {
body.style.background = this.currentTheme.gradient;
console.log('[THEME] Applied gradient:', this.currentTheme.gradient);
}
}
applyThemeToPages() {
// This will be called to apply theme to home.html and other pages
this.saveTheme();
// Send theme update to host (for settings webview)
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', this.currentTheme);
}
// Fallback: send via postMessage (for iframe embedding)
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: 'theme-update',
theme: this.currentTheme
}, '*');
}
} catch (e) {
console.log('Could not send theme update to parent window');
}
}
saveCustomTheme() {
const themeName = prompt('Enter a name for your custom theme:', 'My Custom Theme');
if (themeName) {
const customThemes = this.getCustomThemes();
customThemes[themeName.toLowerCase().replace(/\s+/g, '-')] = {
...this.currentTheme,
name: themeName
};
localStorage.setItem('customThemes', JSON.stringify(customThemes));
// Show success message
this.showMessage('Custom theme saved successfully!', 'success');
}
}
exportTheme() {
const themeData = {
...this.currentTheme,
exportedAt: new Date().toISOString(),
version: '1.0'
};
const blob = new Blob([JSON.stringify(themeData, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `nebula-theme-${themeData.name.toLowerCase().replace(/\s+/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showMessage('Theme exported successfully!', 'success');
}
importTheme(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const themeData = JSON.parse(e.target.result);
// Validate theme structure
if (this.validateTheme(themeData)) {
this.currentTheme = themeData;
this.saveTheme();
this.loadCurrentTheme();
this.updatePreview();
this.applyThemeToCurrentPage();
this.applyThemeToPages();
this.showMessage('Theme imported successfully!', 'success');
} else {
this.showMessage('Invalid theme file format.', 'error');
}
} catch (error) {
this.showMessage('Error reading theme file.', 'error');
}
};
reader.readAsText(file);
}
validateTheme(theme) {
return theme &&
theme.colors &&
theme.colors.bg &&
theme.colors.primary &&
theme.colors.accent &&
theme.colors.text;
}
resetToDefault() {
if (confirm('Are you sure you want to reset to the default theme? This will lose your current customizations.')) {
this.currentTheme = { ...this.defaultTheme };
this.activeThemeName = 'default';
this.saveTheme();
this.saveActiveThemeName('default');
this.loadCurrentTheme();
this.updatePreview();
this.applyThemeToCurrentPage();
this.applyThemeToPages();
this.updateThemeButtons('default');
this.showMessage('Theme reset to default.', 'success');
}
}
saveTheme() {
localStorage.setItem('currentTheme', JSON.stringify(this.currentTheme));
}
loadTheme() {
const savedTheme = localStorage.getItem('currentTheme');
return savedTheme ? JSON.parse(savedTheme) : { ...this.defaultTheme };
}
saveActiveThemeName(themeName) {
localStorage.setItem('activeThemeName', themeName);
}
loadActiveThemeName() {
return localStorage.getItem('activeThemeName') || 'default';
}
restoreActiveThemeButton() {
// First, remove active class from all buttons
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
// If no active theme name is saved, try to detect which predefined theme matches current theme
if (!this.activeThemeName) {
this.activeThemeName = this.detectMatchingPredefinedTheme();
if (this.activeThemeName) {
this.saveActiveThemeName(this.activeThemeName);
} else {
// If no predefined theme matches, this is a custom theme
this.activeThemeName = 'custom';
this.saveActiveThemeName('custom');
}
}
// Update the custom theme button visibility
this.updateCustomThemeButton();
// Then, add active class to the currently active theme button
const activeBtn = document.querySelector(`[data-theme="${this.activeThemeName}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
detectMatchingPredefinedTheme() {
// Check if current theme matches any predefined theme
for (const [themeName, themeData] of Object.entries(this.predefinedThemes)) {
if (this.themesMatch(this.currentTheme, themeData)) {
return themeName;
}
}
return null;
}
themesMatch(theme1, theme2) {
// Compare essential properties to determine if themes match
return theme1.colors.bg === theme2.colors.bg &&
theme1.colors.darkPurple === theme2.colors.darkPurple &&
theme1.colors.primary === theme2.colors.primary &&
theme1.colors.accent === theme2.colors.accent &&
theme1.colors.text === theme2.colors.text &&
theme1.layout === theme2.layout &&
theme1.showLogo === theme2.showLogo &&
theme1.customTitle === theme2.customTitle;
}
getCustomThemes() {
const customThemes = localStorage.getItem('customThemes');
return customThemes ? JSON.parse(customThemes) : {};
}
showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${type}`;
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s ease;
background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#e53e3e' : '#4299e1'};
`;
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 300);
}, 3000);
}
// Static method to apply theme to any page
static applyThemeToPage() {
const savedTheme = localStorage.getItem('currentTheme');
if (savedTheme) {
const theme = JSON.parse(savedTheme);
const root = document.documentElement;
root.style.setProperty('--bg', theme.colors.bg);
root.style.setProperty('--dark-blue', theme.colors.darkBlue);
root.style.setProperty('--dark-purple', theme.colors.darkPurple);
root.style.setProperty('--primary', theme.colors.primary);
root.style.setProperty('--accent', theme.colors.accent);
root.style.setProperty('--text', theme.colors.text);
root.style.setProperty('--url-bar-bg', theme.colors.urlBarBg);
root.style.setProperty('--url-bar-text', theme.colors.urlBarText);
root.style.setProperty('--url-bar-border', theme.colors.urlBarBorder);
root.style.setProperty('--tab-bg', theme.colors.tabBg);
root.style.setProperty('--tab-text', theme.colors.tabText);
root.style.setProperty('--tab-active', theme.colors.tabActive);
root.style.setProperty('--tab-active-text', theme.colors.tabActiveText);
root.style.setProperty('--tab-border', theme.colors.tabBorder);
// Apply gradient to body if it exists
const body = document.body;
if (body && theme.gradient) {
body.style.background = theme.gradient;
console.log('[THEME] Applied gradient from storage:', theme.gradient);
}
return theme;
}
return null;
}
}
// Auto-initialize on settings page
if (window.location.pathname.includes('settings.html')) {
document.addEventListener('DOMContentLoaded', () => {
window.browserCustomizer = new BrowserCustomizer();
});
}
// Add keyframe animations for messages
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
+1024
View File
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
// Unified icon set loaders with graceful fallbacks.
// Each loader returns an array of string icon names (NOT SVG markup) suitable for name-based selection.
// Some libraries don't have an easy metadata endpoint; we attempt a fetch and fall back to a small curated subset.
import { fetchAllIcons as fetchMaterialIcons, icons as materialFallback } from './icons.js';
async function attemptJSON(url, transform) {
try {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
const data = await res.json();
return transform ? transform(data) : data;
} catch (e) {
console.warn('[IconSets] Failed to fetch', url, e);
return null;
}
}
// --- SVG helpers ---
async function attemptText(url) {
try {
const res = await fetch(url, { cache: 'force-cache' });
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
const txt = await res.text();
if (!/^<svg[\s\S]*<\/svg>$/i.test(txt.trim())) throw new Error('Not SVG');
return txt;
} catch {
return null;
}
}
function svgToDataUrl(svg) {
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg.replace(/<script[\s\S]*?<\/script>/gi, ''));
}
const staticFallbacks = {
lucide: ['activity','airplay','alarm-clock','align-center','anchor','apple','archive','arrow-big-up','at-sign','award','battery','bell','bluetooth','book','bookmark','briefcase','calendar','camera','cast','check','chevron-down','chrome','cloud','code','command','compass','cpu','database','download','edit','external-link','eye','file','folder','gamepad','globe','heart','help-circle','home','image','info','keyboard','layers','link','list','lock','mail','map','menu','mic','moon','music','package','pie-chart','play','plus','pocket','power','refresh-ccw','rss','save','scissors','search','settings','share','shield','smartphone','speaker','star','sun','tablet','tag','terminal','thumbs-up','trash','tv','twitter','upload','user','video','wifi','x','zap'],
tabler: ['activity','alarm','affiliate','anchor','api','app-window','apple','archive','armchair','arrow-down','at','award','backspace','ballon','battery','bell','bluetooth','bolt','book','bookmark','briefcase','browser','bug','building','calendar','camera','car','chart-area','chart-bar','chart-pie','chart-scatter','check','chevron-down','cloud','code','coffee','color-swatch','command','compass','cpu','credit-card','dashboard','database','device-desktop','device-mobile','dice','dna','download','drop','edit','file','filter','flag','flame','folder','gift','globe','grid','hash','headphones','heart','help','home','id','inbox','info-circle','key','keyboard','language','layers','layout','layout-grid','letter-a','link','lock','login','logout','mail','map','menu','message','microphone','mood-happy','moon','music','news','note','package','password','phone','photo','player-play','plug','plus','power','printer','puzzle','refresh','rocket','route','rss','school','search','server','settings','share','shield','smart-home','snowflake','sparkles','star','sun','switch','tag','thumb-up','tool','trash','trophy','typography','upload','user','video','wifi','world','x'],
phosphor: ['activity','airplane','anchor','apple-logo','archive','arrow-down','arrow-up','at','bag','bell','book','bookmark','bounding-box','briefcase','browser','bug','calendar','camera','car','check','clipboard','cloud','code','command','compass','cpu','credit-card','database','device-mobile','device-tablet','door','download','drop','envelope','eye','eyedropper','file','film-strip','flag','flame','folder','funnel','game-controller','gear','globe','hand','hash','headphones','heart','house','image','info','key','keyboard','leaf','link','lock','magnet','magnifying-glass','map-pin','microphone','moon','music-note','note','nut','package','paper-plane','paperclip','path','pen','phone','plug','plus','power','printer','question','rocket','rss','scissors','share','shield','shopping-cart','sketch-logo','smiley','sparkle','speaker-high','star','sun','swatches','tag','terminal','thumbs-up','toolbox','trash','trophy','tv','user','users','video-camera','wifi-high','x','yarn','youtube-logo','zap'],
remix: ['add','alarm','alert','anchor','apps','archive','arrow-down','arrow-right','arrow-up','at','award','bank','bar-chart','battery','bell','bluetooth','book','bookmark','briefcase','bug','building','calendar','camera','car','chat','chrome','clipboard','cloud','code','command','compass','copyleft','copyright','cpu','dashboard','database','delete-bin','device','dice','download','dribbble','drive','earth','edge','edit','facebook','file','filter','fire','flag','folder','gamepad','gift','github','gitlab','global','google','group','hard-drive','heart','home','image','inbox','instagram','keyboard','keynote','layout','links','list','lock','login','logout','mac','mail','map','menu','message','mic','moon','music','notification','paragraph','pause','phone','picture-in-picture','play','plug','price-tag','print','qr-code','question','reddit','refresh','restart','rocket','rss','scales','search','secure-payment','send','settings','share','shield','shopping-bag','slack','smartphone','sound-module','star','sun','t-box','tablet','tag','telegram','thumb-up','timer','tool','trophy','twitter','tv','upload','usb','user','video','visa','voicemail','volume-up','wallet','wifi','windows','xbox','youtube','zoom-in'],
bootstrap: ['alarm','android','apple','archive','arrow-down','arrow-up','arrow-left','arrow-right','at','award','backspace','badge-4k','bag','bank','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','brush','bug','calendar','camera','card-image','card-list','cart','chat','check','chevron-down','circle','cloud','code','command','compass','cpu','credit-card','database','device-hdd','device-ssd','display','download','droplet','earbuds','emoji-smile','envelope','exclamation','eye','facebook','file','filter','flag','folder','funnel','gear','gift','globe','google','graph-up','grid','hammer','hand-thumbs-up','hash','headphones','heart','house','image','info','instagram','joystick','keyboard','laptop','layers','layout-split','lightning','link','lock','mailbox','map','megaphone','menu-button','mic','moon','music-note','nut','palette','paperclip','patch-check','pen','pencil','people','phone','pin','play','plug','plus','power','printer','qr-code','question','rocket','rss','save','scissors','search','server','share','shield','shop','skip-forward','slack','speaker','speedometer','star','sun','tablet','tag','terminal','tools','trash','trophy','truck','twitch','twitter','type','ui-checks','upload','usb','vector-pen','wallet','whatsapp','wifi','windows','wrench','x','youtube'],
heroicons: ['academic-cap','adjustments-horizontal','adjustments-vertical','archive-box','arrow-down','arrow-up','arrow-right','arrow-left','at-symbol','backspace','banknotes','bars-2','bars-3','battery-100','beaker','bell','bookmark','briefcase','cake','calendar','camera','chart-bar','chat-bubble-bottom-center','chat-bubble-left','check','chevron-down','chip','circle-stack','cloud','code-bracket','cog','command-line','computer-desktop','cpu-chip','cube','currency-dollar','device-phone-mobile','device-tablet','document','document-text','ellipsis-horizontal','envelope','exclamation-circle','eye','film','finger-print','fire','flag','folder','gift','globe-alt','hand-thumb-up','heart','home','identification','inbox','information-circle','key','language','lifebuoy','light-bulb','link','lock-closed','magnifying-glass','map','megaphone','microphone','moon','musical-note','newspaper','paint-brush','paper-airplane','paper-clip','phone','photo','play','plus','power','printer','puzzle-piece','qr-code','question-mark-circle','rocket-launch','rss','scale','scissors','server','share','shield-check','sparkles','square-3-stack-3d','star','sun','swatch','tag','trophy','tv','user','users','video-camera','wallet','wifi','wrench','x-mark'],
feather: ['activity','airplay','alert-circle','alert-triangle','anchor','aperture','archive','at-sign','award','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','calendar','camera','cast','check','chevron-down','chrome','circle','clipboard','cloud','code','command','compass','cpu','database','download','droplet','edit','eye','facebook','file','film','filter','flag','folder','gift','git-branch','git-commit','git-merge','github','gitlab','globe','grid','hash','headphones','heart','help-circle','home','image','info','instagram','key','layers','layout','link','lock','mail','map','menu','mic','monitor','moon','music','package','paperclip','pause','pen-tool','phone','play','plus','pocket','power','printer','radio','refresh-ccw','refresh-cw','repeat','rewind','rss','save','scissors','search','send','server','settings','share','shield','shopping-bag','shopping-cart','shuffle','slack','smartphone','speaker','square','star','sun','tablet','tag','target','terminal','thumbs-up','tool','trash','trello','trending-up','triangle','truck','tv','twitter','type','umbrella','unlock','upload','user','users','video','voicemail','volume','watch','wifi','wind','x','zap'],
simple: ['github','gitlab','google','youtube','twitter','facebook','twitch','discord','spotify','apple','microsoft','android','linux','ubuntu','x','linkedin','npm','pypi','docker','kubernetes','aws','azure','gcp','cloudflare','figma','notion','slack','whatsapp','meta','paypal','stripe','reddit','snapchat','steam','xbox','playstation','nintendo','instagram','pinterest','soundcloud','openai','vercel','netlify','digitalocean'],
radix: ['activity-log','airplane','backpack','bell','bookmark','calendar','camera','card-stack','caret-down','caret-up','chat-bubble','chat-dots','check','chevron-down','chevron-left','chevron-right','chevron-up','clock','code','component-1','component-2','cookie','copy','cube','discord-logo','double-arrow-down','double-arrow-left','double-arrow-right','double-arrow-up','drag-handle-dots-2','envelope-closed','envelope-open','exclamation-triangle','external-link','eye-open','file','file-text','file-plus','gear','globe','heart','home','image','info-circled','keyboard','laptop','layers','link-1','link-2','lock-closed','magic-wand','magnifying-glass','moon','notebook','open-in-new-window','paper-plane','pencil-1','person','pie-chart','pin-left','pin-right','plus','question-mark-circled','reload','rocket','rows','scissors','share-1','share-2','shield','speaker-loud','star','sun','target','trash','upload','video','zoom-in','zoom-out']
};
export const iconSets = {
material: {
label: 'Material',
loader: async () => { try { return await fetchMaterialIcons(); } catch { return materialFallback; } },
fetchIcon: async () => null
},
lucide: {
label: 'Lucide',
loader: async () => {
const data = await attemptJSON('https://cdn.jsdelivr.net/npm/lucide@latest/dist/metadata.json', d => Object.keys(d));
return data && data.length ? data : staticFallbacks.lucide;
},
fetchIcon: async (name) => {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${name}.svg`);
return svg ? svgToDataUrl(svg) : null;
}
},
tabler: {
label: 'Tabler',
loader: async () => {
const data = await attemptJSON('https://cdn.jsdelivr.net/gh/tabler/tabler-icons@latest/icons.json', d => d.map(o => o.name));
return data && data.length ? data : staticFallbacks.tabler;
},
fetchIcon: async (name) => {
const urls = [
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/outline/${name}.svg`,
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/filled/${name}.svg`
];
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
return null;
}
},
phosphor: {
label: 'Phosphor',
loader: async () => staticFallbacks.phosphor,
fetchIcon: async (name) => {
const styles = ['regular','bold','duotone','fill','light','thin'];
for (const style of styles) {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/${style}/${name}.svg`);
if (svg) return svgToDataUrl(svg);
}
return null;
}
},
remix: {
label: 'Remix',
loader: async () => staticFallbacks.remix,
fetchIcon: async () => null,
fontClass: (name) => `ri-${name}-line` // use line style font sprite
},
bootstrap: {
label: 'Bootstrap',
loader: async () => staticFallbacks.bootstrap,
fetchIcon: async (name) => {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/icons/${name}.svg`);
return svg ? svgToDataUrl(svg) : null;
},
fontClass: (name) => `bi-${name}`
},
heroicons: {
label: 'Heroicons',
loader: async () => staticFallbacks.heroicons,
fetchIcon: async (name) => {
const urls = [
`https://cdn.jsdelivr.net/npm/heroicons@2/24/outline/${name}.svg`,
`https://cdn.jsdelivr.net/npm/heroicons@2/24/solid/${name}.svg`
];
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
return null;
}
},
feather: {
label: 'Feather',
loader: async () => staticFallbacks.feather,
fetchIcon: async (name) => {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/feather-icons@4/dist/icons/${name}.svg`);
return svg ? svgToDataUrl(svg) : null;
},
fontClass: (name) => `icon-${name}` // fallback for display
},
simple: {
label: 'Simple Icons',
loader: async () => staticFallbacks.simple,
fetchIcon: async (name) => {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${name}.svg`);
return svg ? svgToDataUrl(svg) : null;
}
},
radix: {
label: 'Radix',
loader: async () => staticFallbacks.radix,
fetchIcon: async (name) => {
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@radix-ui/icons@latest/icons/${name}.svg`);
return svg ? svgToDataUrl(svg) : null;
}
}
};
// Utility: get list of set keys + label for UI
export function listIconSets() {
return Object.entries(iconSets).map(([key, val]) => ({ key, label: val.label }));
}
+21
View File
@@ -0,0 +1,21 @@
// This file is automatically generated from Google's Material Icons.
/**
* Fetches the full list of Material Icon names from Google Fonts.
* Returns an array of strings like ["3d_rotation","access_alarm",…]
*/
export async function fetchAllIcons() {
const res = await fetch("https://fonts.google.com/metadata/icons");
let txt = await res.text();
// strip the weird prefix )]}'\n
txt = txt.replace(/^\)\]\}'\s*/, "");
const json = JSON.parse(txt);
return json.icons.map(icon => icon.name);
}
// Fallback static array for immediate use (e.g. the "+" button and bookmark icons)
export const icons = [
'add',
'bookmark',
'star',
// …add any other icons your components expect synchronously…
];
+49
View File
@@ -0,0 +1,49 @@
const zoomPercentEl = document.getElementById('zoom-percent');
function setCssVar(name, value, fallback) {
const val = value || fallback;
if (val) document.documentElement.style.setProperty(name, val);
}
function applyTheme(theme) {
const colors = theme?.colors || theme || {};
setCssVar('--bg', colors.bg, '#0b0d10');
setCssVar('--dark-blue', colors.darkBlue, '#0b1c2b');
setCssVar('--dark-purple', colors.darkPurple, '#1b1035');
setCssVar('--primary', colors.primary, '#7b2eff');
setCssVar('--accent', colors.accent, '#00c6ff');
setCssVar('--text', colors.text, '#e0e0e0');
setCssVar('--url-bar-bg', colors.urlBarBg, '#1c2030');
setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
}
async function refreshZoom() {
if (!window.electronAPI?.invoke || !zoomPercentEl) return;
try {
const z = await window.electronAPI.invoke('get-zoom-factor');
zoomPercentEl.textContent = `${Math.round(z * 100)}%`;
} catch {}
}
window.electronAPI?.on?.('menu-popup-init', (payload) => {
applyTheme(payload?.theme);
refreshZoom();
});
window.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-cmd]');
if (!btn) return;
const cmd = btn.getAttribute('data-cmd');
window.electronAPI?.send?.('menu-popup-command', { cmd });
if (cmd === 'zoom-in' || cmd === 'zoom-out') {
setTimeout(refreshZoom, 50);
}
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' });
}
});
refreshZoom();
+38
View File
@@ -0,0 +1,38 @@
const SEARCH_URL = 'https://www.google.com/search?q=';
function toNavigationUrl(input) {
const value = (input || '').trim();
if (!value) return null;
if (/^(https?:|file:|data:|blob:)/i.test(value)) return value;
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
return `${SEARCH_URL}${encodeURIComponent(value)}`;
}
function rememberSearch(input) {
if (!input || input.includes('.') || /^(https?:|file:)/i.test(input)) return;
try {
const current = JSON.parse(localStorage.getItem('searchHistory') || '[]');
const next = [input, ...current.filter(item => item !== input)].slice(0, 100);
localStorage.setItem('searchHistory', JSON.stringify(next));
} catch {}
}
function navigateTo(input) {
const target = toNavigationUrl(input);
if (!target) return;
rememberSearch(input.trim());
window.location.href = target;
}
window.NebulaCEF = { navigateTo, toNavigationUrl };
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('start-form');
const urlInput = document.getElementById('start-url');
if (!form || !urlInput) return;
form.addEventListener('submit', event => {
event.preventDefault();
navigateTo(urlInput.value);
});
});
+901
View File
@@ -0,0 +1,901 @@
// Prefer contextBridge-exposed API
const ipc = (window.electronAPI && typeof window.electronAPI.invoke === 'function')
? window.electronAPI
: null;
let clearBtn = document.getElementById('clear-data-btn');
const statusDiv = document.getElementById('status');
const statusText = document.getElementById('status-text');
const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f'
const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh)
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh)
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl'
const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300)
function showStatus(message) {
if (statusText && statusDiv) {
statusText.textContent = message;
statusDiv.classList.remove('hidden');
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 2000);
} else {
console.log('[STATUS]', message);
}
}
function showStatus(message) {
if (!statusText || !statusDiv) {
console.log('[STATUS]', message);
return;
}
statusText.textContent = message;
statusDiv.classList.remove('hidden');
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 2000);
}
function attachClearHandler(btn) {
if (!btn) return;
btn.onclick = async () => {
if (statusDiv && statusText) {
statusDiv.classList.remove('hidden');
statusText.textContent = 'Clearing cookies, storage, cache, and history...';
}
try {
if (ipc) {
const ok = await ipc.invoke('clear-browser-data');
// Also clear localStorage site history in this context
try { localStorage.removeItem('siteHistory'); } catch {}
// Try to refresh lists if present
try { if (typeof loadHistories === 'function') await loadHistories(); } catch {}
showStatus(ok
? 'All browser data cleared.'
: 'Failed to clear browser data.');
} else {
localStorage.clear();
showStatus('Local page data cleared.');
}
} catch (error) {
console.error('Error clearing browser data:', error);
showStatus('An error occurred while clearing data.');
} finally {
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', currentTheme);
}
}
};
}
// Try attaching immediately, and again on DOMContentLoaded
attachClearHandler(clearBtn);
window.addEventListener('DOMContentLoaded', () => {
if (!clearBtn) {
clearBtn = document.getElementById('clear-data-btn');
attachClearHandler(clearBtn);
}
// Wire per-section clear buttons to main when possible
const clearSiteBtn = document.getElementById('clear-site-history-btn');
if (clearSiteBtn) {
clearSiteBtn.addEventListener('click', async () => {
try {
// Clear localStorage copy
try { localStorage.removeItem('siteHistory'); } catch {}
// Ask main to clear file-based history for consistency
if (ipc) { await ipc.invoke('clear-site-history'); }
showStatus('Site history cleared');
try { if (typeof loadHistories === 'function') await loadHistories(); } catch {}
} catch (e) {
console.error('Clear site history error:', e);
showStatus('Failed clearing site history');
}
});
}
const clearSearchBtn = document.getElementById('clear-search-history-btn');
if (clearSearchBtn) {
clearSearchBtn.addEventListener('click', async () => {
try {
// Clear from localStorage in this context
try { localStorage.removeItem('searchHistory'); } catch {}
if (ipc) { await ipc.invoke('clear-search-history'); }
showStatus('Search history cleared');
} catch (e) {
console.error('Clear search history error:', e);
showStatus('Failed clearing search history');
}
});
}
// Weather unit controls
try {
const stored = localStorage.getItem(WEATHER_UNIT_KEY) || 'auto';
const radios = document.querySelectorAll('input[name="weather-unit"]');
radios.forEach(r => r.checked = (r.value === stored));
radios.forEach(radio => radio.addEventListener('change', () => {
const val = document.querySelector('input[name="weather-unit"]:checked')?.value || 'auto';
localStorage.setItem(WEATHER_UNIT_KEY, val);
showStatus(`Weather units set to ${val === 'c' ? 'Celsius' : val === 'f' ? 'Fahrenheit' : 'Auto'}`);
// Hint home page to refresh weather if it listens to storage events
try { window.dispatchEvent(new StorageEvent('storage', { key: WEATHER_UNIT_KEY, newValue: val })); } catch {}
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('settings-update', { weatherUnit: val });
}
}));
} catch (e) { console.warn('Weather unit setup failed', e); }
// Home layout controls
try {
const searchRange = document.getElementById('home-search-y');
const searchVal = document.getElementById('home-search-y-val');
const bmRange = document.getElementById('home-bookmarks-y');
const bmVal = document.getElementById('home-bookmarks-y-val');
const cornerRadios = document.querySelectorAll('input[name="home-glance-corner"]');
const initNum = (key, def, input, label) => {
const v = Number(localStorage.getItem(key) || def);
if (input) input.value = String(v);
if (label) label.textContent = v + 'vh';
return v;
};
initNum(HOME_SEARCH_Y_KEY, 22, searchRange, searchVal);
initNum(HOME_BOOKMARKS_Y_KEY, 40, bmRange, bmVal);
const storedCorner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
cornerRadios.forEach(r => r.checked = (r.value === storedCorner));
const notify = () => {
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('settings-update', {
searchY: Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22),
bookmarksY: Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40),
glanceCorner: localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'
});
}
};
if (searchRange) searchRange.addEventListener('input', () => {
const val = Number(searchRange.value);
searchVal.textContent = val + 'vh';
localStorage.setItem(HOME_SEARCH_Y_KEY, String(val));
notify();
});
if (bmRange) bmRange.addEventListener('input', () => {
const val = Number(bmRange.value);
bmVal.textContent = val + 'vh';
localStorage.setItem(HOME_BOOKMARKS_Y_KEY, String(val));
notify();
});
cornerRadios.forEach(r => r.addEventListener('change', () => {
const val = document.querySelector('input[name="home-glance-corner"]:checked')?.value || 'br';
localStorage.setItem(HOME_GLANCE_CORNER_KEY, val);
notify();
}));
} catch (e) { console.warn('Home layout control setup failed', e); }
// Display scale controls
try {
const scaleValue = document.getElementById('display-scale-value');
const zoomDecrease = document.getElementById('zoom-decrease');
const zoomIncrease = document.getElementById('zoom-increase');
const zoomPresets = document.querySelectorAll('.zoom-preset-btn');
let currentScale = Number(localStorage.getItem(DISPLAY_SCALE_KEY) || 100);
// Function to apply zoom
async function applyZoom(scale) {
currentScale = Math.max(50, Math.min(300, scale));
if (scaleValue) scaleValue.textContent = currentScale + '%';
localStorage.setItem(DISPLAY_SCALE_KEY, String(currentScale));
// Highlight active preset
zoomPresets.forEach(btn => {
btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale);
});
if (ipc && typeof ipc.invoke === 'function') {
try {
const zoomFactor = currentScale / 100;
await ipc.invoke('set-zoom-factor', zoomFactor);
showStatus(`Zoom set to ${currentScale}%`);
} catch (err) {
console.warn('Failed to apply zoom:', err);
showStatus(`Zoom saved to ${currentScale}%`);
}
}
}
// Initialize display
if (scaleValue) scaleValue.textContent = currentScale + '%';
zoomPresets.forEach(btn => {
btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale);
});
// Apply saved zoom on load
if (ipc && typeof ipc.invoke === 'function' && currentScale !== 100) {
try {
const zoomFactor = currentScale / 100;
ipc.invoke('set-zoom-factor', zoomFactor).catch(err => {
console.warn('Failed to apply initial zoom:', err);
});
} catch (err) {
console.warn('Failed to apply initial zoom:', err);
}
}
// Decrease button
if (zoomDecrease) {
zoomDecrease.addEventListener('click', () => {
applyZoom(currentScale - 10);
});
}
// Increase button
if (zoomIncrease) {
zoomIncrease.addEventListener('click', () => {
applyZoom(currentScale + 10);
});
}
// Preset buttons
zoomPresets.forEach(btn => {
btn.addEventListener('click', () => {
const zoom = Number(btn.dataset.zoom);
applyZoom(zoom);
});
});
} catch (e) { console.warn('Display scale setup failed', e); }
// Big Picture Mode controls
try {
const bigPictureBtn = document.getElementById('launch-bigpicture-btn');
const bigPictureStatus = document.getElementById('bigpicture-status');
// Check if Big Picture Mode is recommended for this display
if (window.bigPictureAPI && typeof window.bigPictureAPI.isSuggested === 'function') {
window.bigPictureAPI.isSuggested().then(suggested => {
if (suggested && bigPictureStatus) {
bigPictureStatus.textContent = '✓ Recommended for your display';
bigPictureStatus.style.color = '#4ade80';
}
}).catch(() => {});
// Get screen info for display
window.bigPictureAPI.getScreenInfo().then(info => {
if (info && bigPictureStatus) {
const hint = info.isSteamDeck ? 'Steam Deck detected' :
info.isSmallScreen ? 'Small screen detected' : '';
if (hint && !bigPictureStatus.textContent) {
bigPictureStatus.textContent = hint;
}
}
}).catch(() => {});
}
if (bigPictureBtn) {
bigPictureBtn.addEventListener('click', async () => {
try {
if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') {
showStatus('Launching Big Picture Mode...');
await window.bigPictureAPI.launch();
} else {
showStatus('Big Picture Mode not available');
}
} catch (e) {
console.error('Big Picture Mode launch error:', e);
showStatus('Failed to launch Big Picture Mode');
}
});
}
} catch (e) { console.warn('Big Picture Mode setup failed', e); }
});
// Tabs: simple controller
function activateTab(tabName) {
const links = document.querySelectorAll('.tab-link');
const panels = document.querySelectorAll('.tab-panel');
links.forEach(l => {
const isActive = l.dataset.tab === tabName;
l.classList.toggle('active', isActive);
l.setAttribute('aria-selected', isActive ? 'true' : 'false');
if (isActive) l.focus({ preventScroll: true });
});
panels.forEach(p => {
const isActive = p.id === `panel-${tabName}`;
p.classList.toggle('active', isActive);
p.hidden = !isActive;
// noop
});
try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {}
}
function initTabs() {
const links = document.querySelectorAll('.tab-link');
const getFocusableElements = (container) => {
if (!container) return [];
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
return Array.from(container.querySelectorAll(selector))
.filter(el => !el.disabled && el.getAttribute('aria-hidden') !== 'true' && el.offsetParent !== null);
};
const focusFirstInActivePanel = () => {
const activePanel = document.querySelector('.tab-panel.active') || null;
const focusables = getFocusableElements(activePanel);
if (focusables.length > 0) {
focusables[0].focus({ preventScroll: true });
return true;
}
if (activePanel) {
if (!activePanel.hasAttribute('tabindex')) {
activePanel.setAttribute('tabindex', '-1');
}
activePanel.focus({ preventScroll: true });
return true;
}
return false;
};
// Direct listeners (for accessibility focus handling)
links.forEach((link, index) => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const name = link.dataset.tab;
if (!name) return;
if (location.hash !== `#${name}`) {
history.replaceState(null, '', `#${name}`);
}
activateTab(name);
});
// Controller/keyboard: move from tab to panel content
link.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
const moved = focusFirstInActivePanel();
if (moved) {
e.preventDefault();
e.stopPropagation();
}
}
});
});
// Delegation as a fallback if elements are re-rendered
const tabContainer = document.querySelector('.tabs');
if (tabContainer) {
tabContainer.addEventListener('click', (e) => {
const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null;
if (!btn || !tabContainer.contains(btn)) return;
const name = btn.dataset.tab;
if (!name) return;
if (location.hash !== `#${name}`) {
history.replaceState(null, '', `#${name}`);
}
activateTab(name);
});
}
// Global fallback: if focus is on sidebar tabs, move into active panel on down/right
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (e.key !== 'ArrowDown' && e.key !== 'ArrowRight') return;
const activeEl = document.activeElement;
const inTabs = activeEl && (activeEl.classList?.contains('tab-link') || activeEl.closest?.('.tabs'));
const inSidebar = activeEl && activeEl.closest?.('.sidebar');
if (inTabs || inSidebar) {
const moved = focusFirstInActivePanel();
if (moved) {
e.preventDefault();
e.stopPropagation();
}
}
}, true);
// Resolve initial tab: hash > storage > default 'general'
let initial = (location.hash || '').replace('#', '') || null;
if (!initial) {
try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {}
}
if (!initial) initial = 'general';
activateTab(initial);
}
// Initialize tabs after DOM is ready but before customization init uses the DOM
window.addEventListener('DOMContentLoaded', () => {
initTabs();
});
// Apply current theme to settings page
function applyCurrentThemeToSettings() {
if (!window.BrowserCustomizer) return;
const savedTheme = localStorage.getItem('nebula-theme');
let theme = null;
if (savedTheme) {
try {
theme = JSON.parse(savedTheme);
} catch (e) {
console.warn('Failed to parse saved theme', e);
}
}
if (!theme || !theme.colors) return;
// Apply theme colors to CSS variables
const root = document.documentElement;
root.style.setProperty('--bg', theme.colors.bg || '#121418');
root.style.setProperty('--gradient-end', theme.colors.darkPurple || '#1B1035');
root.style.setProperty('--primary', theme.colors.primary || '#7B2EFF');
root.style.setProperty('--accent', theme.colors.accent || '#00C6FF');
root.style.setProperty('--text', theme.colors.text || '#E0E0E0');
// Update glow colors based on theme
const primaryRgb = hexToRgb(theme.colors.primary || '#7B2EFF');
if (primaryRgb) {
root.style.setProperty('--ring', `0 0 0 2px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.4)`);
root.style.setProperty('--glow-subtle', `0 4px 20px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.15)`);
}
}
// Helper to convert hex to RGB
function hexToRgb(hex) {
const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// Listen for theme changes
window.addEventListener('storage', (e) => {
if (e.key === 'nebula-theme') {
applyCurrentThemeToSettings();
}
});
// About tab population
async function populateAbout() {
try {
const info = (window.aboutAPI && typeof window.aboutAPI.getInfo === 'function')
? await window.aboutAPI.getInfo()
: null;
const byId = (id) => document.getElementById(id);
if (!info || info.error) {
byId('about-app-name').textContent = 'Nebula Browser';
byId('about-app-version').textContent = 'CEF build';
const versionCopy = byId('about-app-version-copy');
if (versionCopy) versionCopy.textContent = 'CEF build';
byId('about-packaged').textContent = 'Native';
byId('about-userdata').textContent = 'Managed by CEF';
byId('about-cef').textContent = navigator.userAgent;
byId('about-chrome').textContent = navigator.userAgent.match(/Chrome\/([^\s]+)/)?.[1] || 'Unknown';
byId('about-node').textContent = 'Not available';
byId('about-v8').textContent = 'Managed by Chromium';
byId('about-os').textContent = navigator.platform || 'Unknown';
byId('about-cpu').textContent = navigator.hardwareConcurrency ? `${navigator.hardwareConcurrency} logical cores` : 'Unknown';
byId('about-arch').textContent = navigator.userAgentData?.platform || navigator.platform || 'Unknown';
byId('about-mem').textContent = navigator.deviceMemory ? `${navigator.deviceMemory} GB estimate` : 'Unknown';
return;
}
byId('about-app-name').textContent = info.appName;
byId('about-app-version').textContent = info.appVersion;
byId('about-packaged').textContent = info.isPackaged ? 'Yes' : 'No';
byId('about-userdata').textContent = info.userDataPath;
byId('about-cef').textContent = info.cefVersion || info.chromeVersion || 'Chromium Embedded Framework';
byId('about-chrome').textContent = info.chromeVersion;
byId('about-node').textContent = info.nodeVersion;
byId('about-v8').textContent = info.v8Version;
byId('about-os').textContent = `${info.osType} ${info.osRelease}`;
byId('about-cpu').textContent = info.cpu;
byId('about-arch').textContent = info.arch;
byId('about-mem').textContent = `${info.totalMemGB} GB`;
const copyBtn = document.getElementById('copy-about-btn');
if (copyBtn && !copyBtn.dataset.listenerAttached) {
copyBtn.dataset.listenerAttached = 'true';
copyBtn.addEventListener('click', async () => {
const payload = [
`Nebula ${info.appVersion} (${info.isPackaged ? 'packaged' : 'dev'})`,
`CEF ${info.cefVersion || info.chromeVersion || 'unknown'} | Chromium ${info.chromeVersion} | V8 ${info.v8Version}`,
`${info.osType} ${info.osRelease} ${info.arch}`,
`CPU: ${info.cpu}`,
`RAM: ${info.totalMemGB} GB`,
`UserData: ${info.userDataPath}`
].join('\n');
try {
await navigator.clipboard.writeText(payload);
showStatus('Diagnostics copied');
} catch (err) {
console.error('Clipboard error:', err);
showStatus('Failed to copy diagnostics');
}
});
}
} catch (err) {
console.error('[ABOUT] Error populating about info:', err);
}
}
// Populate about info after DOM is ready
window.addEventListener('DOMContentLoaded', () => {
populateAbout();
applyCurrentThemeToSettings();
// Refresh about info when About tab is clicked
const aboutTabBtn = document.getElementById('tab-about');
if (aboutTabBtn) {
aboutTabBtn.addEventListener('click', () => {
// Refresh after a short delay to allow tab transition
setTimeout(() => {
populateAbout();
}, 100);
});
}
});
// Electron updater feature setup (for security updates)
async function setupElectronUpdater() {
const securityUpdatesSection = document.querySelector('.customization-group:has(#electron-update-banner)');
const banner = document.getElementById('electron-update-banner');
const statusSpan = document.getElementById('electron-update-status');
const detailsDiv = document.getElementById('electron-update-details');
const progressDiv = document.getElementById('electron-update-progress');
const checkBtn = document.getElementById('check-electron-versions');
const upgradeBtn = document.getElementById('electron-upgrade-btn');
const versionSelect = document.getElementById('electron-version-select');
const currentVersionSpan = document.getElementById('electron-current-version');
const appVersionSpan = document.getElementById('about-app-version-copy');
if (!ipc) {
console.warn('[ELECTRON-UPDATER] IPC not available');
return;
}
// Check if app is packaged - if so, hide the entire Security Updates section
try {
const appInfo = await ipc.invoke('get-app-info');
console.log('[ELECTRON-UPDATER] App info:', appInfo);
if (appInfo && appInfo.isPackaged) {
console.log('[ELECTRON-UPDATER] Packaged build detected - hiding Security Updates section');
if (securityUpdatesSection) {
securityUpdatesSection.style.display = 'none';
}
return;
}
console.log('[ELECTRON-UPDATER] Development mode - showing Security Updates section');
} catch (err) {
console.error('[ELECTRON-UPDATER] Failed to get app info:', err);
// On error, hide the section to be safe
if (securityUpdatesSection) {
securityUpdatesSection.style.display = 'none';
}
return;
}
let availableVersion = null;
let currentVersion = null;
let isUpgrading = false;
// Get current app version
try {
const info = await window.aboutAPI?.getInfo();
if (info && appVersionSpan) {
appVersionSpan.textContent = info.appVersion || 'Unknown';
}
} catch (err) {
console.error('[ELECTRON-UPDATER] Failed to get app version:', err);
}
// Check for Electron updates
const checkVersions = async () => {
if (isUpgrading) return;
try {
checkBtn.disabled = true;
banner.style.display = 'block';
statusSpan.textContent = 'Checking for updates...';
detailsDiv.textContent = '';
progressDiv.style.display = 'none';
upgradeBtn.style.display = 'none';
banner.style.borderColor = 'rgba(123, 46, 255, 0.3)';
banner.style.background = 'rgba(123, 46, 255, 0.1)';
const buildType = versionSelect.value;
const result = await ipc.invoke('get-electron-versions', buildType);
if (result.error) {
statusSpan.textContent = 'Update check failed';
detailsDiv.textContent = result.error;
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
banner.style.background = 'rgba(244, 67, 54, 0.1)';
showStatus(`Failed: ${result.error}`);
} else {
availableVersion = result.available;
currentVersion = result.current;
if (currentVersionSpan) {
currentVersionSpan.textContent = currentVersion || 'Unknown';
}
const isNewer = compareVersions(availableVersion, currentVersion) > 0;
if (isNewer) {
statusSpan.textContent = 'Security update available';
detailsDiv.textContent = `Electron ${availableVersion} is available (you have ${currentVersion}). This update includes security patches and performance improvements.`;
upgradeBtn.style.display = 'inline-block';
upgradeBtn.disabled = false;
banner.style.borderColor = 'rgba(76, 175, 80, 0.5)';
banner.style.background = 'rgba(76, 175, 80, 0.1)';
showStatus(`Update available: ${availableVersion}`);
} else {
statusSpan.textContent = 'Up to date';
detailsDiv.textContent = `You are running the latest ${buildType} version of Electron (${currentVersion}).`;
upgradeBtn.style.display = 'none';
banner.style.borderColor = 'rgba(100, 100, 100, 0.3)';
banner.style.background = 'rgba(100, 100, 100, 0.1)';
showStatus('Electron is up to date');
}
}
} catch (err) {
console.error('[ELECTRON-UPDATER] Check failed:', err);
statusSpan.textContent = 'Update check failed';
detailsDiv.textContent = err.message;
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
banner.style.background = 'rgba(244, 67, 54, 0.1)';
showStatus('Check failed');
} finally {
checkBtn.disabled = false;
}
};
// Install Electron update
const handleUpgrade = async () => {
if (isUpgrading) return;
const buildType = versionSelect.value;
if (!availableVersion) {
showStatus('No update available');
return;
}
const confirmed = confirm(
`Update Electron from ${currentVersion} to ${availableVersion}?\n\nThis will download and install the ${buildType} version, then restart the application.\n\nThis process may take a few minutes.`
);
if (!confirmed) return;
try {
isUpgrading = true;
upgradeBtn.disabled = true;
checkBtn.disabled = true;
versionSelect.disabled = true;
statusSpan.textContent = 'Installing update...';
detailsDiv.textContent = `Downloading and installing Electron ${availableVersion}. Please wait...`;
progressDiv.style.display = 'block';
banner.style.borderColor = 'rgba(255, 193, 7, 0.5)';
banner.style.background = 'rgba(255, 193, 7, 0.1)';
showStatus('Installing Electron update...');
const result = await ipc.invoke('upgrade-electron', buildType);
if (result.success) {
statusSpan.textContent = 'Update installed';
detailsDiv.textContent = 'Electron has been updated successfully. The application will restart now.';
progressDiv.style.display = 'none';
banner.style.borderColor = 'rgba(76, 175, 80, 0.5)';
banner.style.background = 'rgba(76, 175, 80, 0.1)';
showStatus('Update complete - restarting...');
// Restart the app
setTimeout(() => {
if (ipc) {
ipc.invoke('restart-app').catch(err => {
console.error('Restart failed:', err);
showStatus('Please restart the app manually');
});
}
}, 2000);
} else {
throw new Error(result.error || 'Upgrade failed');
}
} catch (err) {
console.error('[ELECTRON-UPDATER] Upgrade failed:', err);
statusSpan.textContent = 'Update failed';
detailsDiv.textContent = `Failed to install update: ${err.message}`;
progressDiv.style.display = 'none';
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
banner.style.background = 'rgba(244, 67, 54, 0.1)';
showStatus(`Update failed: ${err.message}`);
isUpgrading = false;
upgradeBtn.disabled = false;
checkBtn.disabled = false;
versionSelect.disabled = false;
}
};
// Wire up event handlers
if (checkBtn) {
checkBtn.addEventListener('click', checkVersions);
}
if (upgradeBtn) {
upgradeBtn.addEventListener('click', handleUpgrade);
}
if (versionSelect) {
versionSelect.addEventListener('change', () => {
// Reset UI when build type changes
banner.style.display = 'none';
upgradeBtn.style.display = 'none';
upgradeBtn.disabled = true;
availableVersion = null;
});
}
}
// Helper function to compare semantic versions
function compareVersions(v1, v2) {
const parts1 = v1.split('-')[0].split('.').map(x => parseInt(x, 10));
const parts2 = v2.split('-')[0].split('.').map(x => parseInt(x, 10));
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
// Keep settings open when clicking GitHub by asking host to open externally/new tab
window.addEventListener('DOMContentLoaded', () => {
const gh = document.getElementById('github-link');
if (gh) {
gh.addEventListener('click', (e) => {
try {
e.preventDefault();
const url = gh.getAttribute('href');
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('navigate', url, { newTab: true });
} else if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*');
} else {
window.location.href = url;
}
} catch (err) {
console.error('Failed to open GitHub link:', err);
window.open(gh.getAttribute('href'), '_blank');
}
});
}
const help = document.getElementById('help-link');
if (help) {
help.addEventListener('click', (e) => {
try {
e.preventDefault();
const url = help.getAttribute('href');
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('navigate', url, { newTab: true });
} else if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*');
} else {
window.location.href = url;
}
} catch (err) {
console.error('Failed to open Help link:', err);
window.open(help.getAttribute('href'), '_blank');
}
});
}
});
// -----------------------------
// Plugins management (Settings)
// -----------------------------
async function loadPluginsUI() {
const listEl = document.getElementById('plugins-list');
const reloadAllBtn = document.getElementById('plugins-reload-all');
if (!listEl) return;
// Load list
let items = [];
try {
items = (ipc ? await ipc.invoke('plugins-list') : []) || [];
} catch (e) {
console.warn('plugins-list failed', e);
}
listEl.innerHTML = '';
if (!items.length) {
const empty = document.createElement('div');
empty.className = 'plugin-item';
empty.textContent = 'No plugins found';
listEl.appendChild(empty);
} else {
for (const p of items) {
const categories = Array.isArray(p.categories) ? p.categories.filter(x => x && typeof x === 'string') : [];
const authors = Array.isArray(p.authors) ? p.authors.filter(x => x && typeof x === 'string') : [];
const tagsHtml = categories.length ? `<div class="plugin-tags">${categories.map(c => `<span class=\"plugin-tag\">${escapeHtml(c)}</span>`).join('')}</div>` : '';
const authorsHtml = authors.length ? `<div class=\"plugin-authors\"><span class=\"muted\">Authors:</span> ${authors.map(a => `<span class=\"plugin-author\">${escapeHtml(a)}</span>`).join(', ')}</div>` : '';
const row = document.createElement('div');
row.className = 'plugin-item';
row.setAttribute('role', 'listitem');
row.innerHTML = `
<div class="plugin-meta">
<div class="plugin-title">${escapeHtml(p.name)} <span style="opacity:.7;font-weight:400">v${escapeHtml(p.version)}</span></div>
<div class="plugin-desc">${escapeHtml(p.description || '')}</div>
${tagsHtml}
${authorsHtml}
<div class="plugin-desc" style="opacity:.6; font-size:.85em;">${escapeHtml(p.dir)}</div>
</div>
<div class="plugin-actions">
<label style="display:flex; align-items:center; gap:6px;">
<input type="checkbox" class="plugin-enable" ${p.enabled ? 'checked' : ''}>
<span>${p.enabled ? 'Enabled' : 'Disabled'}</span>
</label>
<span class="spacer"></span>
<button class="plugin-reload">Reload</button>
</div>`;
// Wire actions
const enableInput = row.querySelector('input.plugin-enable');
const labelSpan = row.querySelector('label span');
enableInput.addEventListener('change', async () => {
const enabled = enableInput.checked;
try {
if (ipc) await ipc.invoke('plugins-set-enabled', { id: p.id, enabled });
labelSpan.textContent = enabled ? 'Enabled' : 'Disabled';
showStatus(`${p.name}: ${enabled ? 'Enabled' : 'Disabled'}.`);
} catch (e) {
console.error('Failed to toggle plugin', p.id, e);
enableInput.checked = !enabled;
labelSpan.textContent = enableInput.checked ? 'Enabled' : 'Disabled';
showStatus('Failed updating plugin');
}
});
const reloadBtn = row.querySelector('button.plugin-reload');
reloadBtn.addEventListener('click', async () => {
try {
if (ipc) await ipc.invoke('plugins-reload', { id: p.id });
showStatus(`${p.name} reloaded.`);
} catch (e) {
console.error('Plugin reload failed', e);
showStatus('Reload failed');
}
});
listEl.appendChild(row);
}
}
if (reloadAllBtn) reloadAllBtn.onclick = async () => {
try { if (ipc) await ipc.invoke('plugins-reload', {}); showStatus('Plugins reloaded.'); } catch { showStatus('Reload failed'); }
};
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[c]));
}
// Load when settings page shows Plugins tab for the first time
window.addEventListener('DOMContentLoaded', () => {
const tabBtn = document.getElementById('tab-plugins');
if (!tabBtn) return;
let loaded = false;
const ensureLoad = () => { if (!loaded) { loaded = true; loadPluginsUI(); } };
tabBtn.addEventListener('click', ensureLoad);
if (location.hash === '#plugins') ensureLoad();
});
+611
View File
@@ -0,0 +1,611 @@
/**
* 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: []
};
const nativeApi = window.api || {
async getAllThemes() {
return {
default: {
default: {
name: 'Default',
description: 'Classic Nebula theme',
colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' }
}
}
};
},
async isDefaultBrowser() {
return false;
},
async setAsDefaultBrowser() {
return { success: false, error: 'Default browser setup is handled by the native CEF app.' };
},
async applyTheme(themeId) {
localStorage.setItem('activeThemeName', themeId);
},
async completeFirstRun(data) {
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
}
};
// 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 nativeApi.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;
}
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string') return null;
let normalized = hex.trim().replace(/^#/, '');
if (normalized.length === 3) {
normalized = normalized.split('').map(char => char + char).join('');
}
if (!/^[a-fA-F\d]{6}$/.test(normalized)) return null;
const intValue = parseInt(normalized, 16);
return {
r: (intValue >> 16) & 255,
g: (intValue >> 8) & 255,
b: intValue & 255
};
}
/**
* 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');
setVar('--success', colors.accent, '#4CAF50');
setVar('--warning', colors.primary, '#FF9800');
const primaryRgb = hexToRgb(colors.primary || '#7B2EFF');
const accentRgb = hexToRgb(colors.accent || '#00C6FF');
const successRgb = hexToRgb(colors.accent || '#4CAF50');
const warningRgb = hexToRgb(colors.primary || '#FF9800');
if (primaryRgb) {
setVar('--primary-rgb', `${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}`);
}
if (accentRgb) {
setVar('--accent-rgb', `${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}`);
}
if (successRgb) {
setVar('--success-rgb', `${successRgb.r}, ${successRgb.g}, ${successRgb.b}`);
}
if (warningRgb) {
setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`);
}
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 nativeApi.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 nativeApi.setAsDefaultBrowser();
if (result.success) {
const isDefault = await window.api.isDefaultBrowser();
if (isDefault) {
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);
return;
}
if (statusEl) {
statusEl.classList.remove('not-default');
statusEl.innerHTML = `
<div class="status-icon">️</div>
<p class="status-text">System settings opened. Choose Nebula as your default browser to finish.</p>
`;
}
if (btn) {
btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">↻</span> Check Again';
}
if (result.needsUserAction && nativeApi.openDefaultBrowserSettings) {
try { await nativeApi.openDefaultBrowserSettings(); } catch {}
}
return;
}
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>
`;
}
/**
* Complete setup and save preferences
*/
async function completeSetup() {
console.log('[Setup] Completing first-time setup...', setupState);
try {
// Apply selected theme
await nativeApi.applyTheme(setupState.selectedTheme);
// Save first-run completion
await nativeApi.completeFirstRun({
selectedTheme: setupState.selectedTheme,
defaultBrowserSet: setupState.defaultBrowserSet,
skipped: setupState.skipped
});
console.log('[Setup] First-time setup completed successfully');
window.location.href = 'home.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 nativeApi.completeFirstRun({
selectedTheme: 'default',
defaultBrowserSet: false,
skipped: true
});
console.log('[Setup] Setup skipped, using defaults');
window.location.href = 'home.html';
} catch (error) {
console.error('[Setup] Error skipping setup:', error);
window.location.href = 'home.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);
}
});