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:
+3037
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 }));
|
||||
}
|
||||
@@ -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…
|
||||
];
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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) => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[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
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user