Add theme customization system and theme manager

Introduces a full browser customization system with support for theme presets, live editing, import/export, and non-destructive design. Adds new documentation for customization, updates the features list, and implements a ThemeManager for loading and managing themes at the application level. Updates home and settings pages to support live theming and preview, and adds four built-in themes (default, forest, ocean, sunset).
This commit is contained in:
2025-07-29 11:53:30 +12:00
parent 486bb99cc4
commit aef9b816db
13 changed files with 1176 additions and 7 deletions
+1
View File
@@ -94,6 +94,7 @@ This project is licensed under the MIT License. [Read More](documentation/MIT.md
* [MIT Licese](documentation/MIT.md) * [MIT Licese](documentation/MIT.md)
* [GPU Fix](documentation/GPU-FIX-README.md) * [GPU Fix](documentation/GPU-FIX-README.md)
* [Features](documentation/FEATURES.md) * [Features](documentation/FEATURES.md)
* [Customization](documentation/Customization.md)
* [Project Structure](documentation/PROJECT_STRUCTURE.md) * [Project Structure](documentation/PROJECT_STRUCTURE.md)
* [Core Concepts](documentation/CORE_CONCEPTS.md) * [Core Concepts](documentation/CORE_CONCEPTS.md)
* [Contributing Guide](documentation/CONTRIBUTING.md) * [Contributing Guide](documentation/CONTRIBUTING.md)
+68
View File
@@ -0,0 +1,68 @@
# Nebula Browser Themes
This directory contains theme files for the Nebula Browser customization system.
## Theme Structure
Each theme is a JSON file with the following structure:
```json
{
"name": "Theme Name",
"colors": {
"bg": "#121418",
"darkBlue": "#0B1C2B",
"darkPurple": "#1B1035",
"primary": "#7B2EFF",
"accent": "#00C6FF",
"text": "#E0E0E0"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #121418 0%, #1B1035 100%)",
"version": "1.0",
"description": "Theme description"
}
```
## Color Properties
- `bg`: Main background color
- `darkBlue`: Secondary dark blue accent
- `darkPurple`: Secondary dark purple accent
- `primary`: Primary accent color (used for buttons, logos)
- `accent`: Secondary accent color (used for highlights)
- `text`: Main text color
## Layout Options
- `centered`: Default centered layout
- `sidebar`: Sidebar navigation layout
- `compact`: Compact view layout
## Directories
- `/downloaded/`: Themes downloaded from the community
- `/user/`: User-created custom themes
## Usage
1. **Import Theme**: Go to Settings > Customization > Import Theme
2. **Export Theme**: Create your custom theme and export it
3. **Share Themes**: Share your exported .json files with other users
## Creating Custom Themes
1. Go to Settings > Browser Customization
2. Adjust colors and settings using the controls
3. Use the live preview to see changes
4. Save as custom theme or export to share
## Community Themes
Place downloaded community themes in the `/downloaded/` folder. The browser will automatically detect and make them available in the theme selector.
## Non-Destructive Design
All theme changes are stored separately and can be reset to default at any time. Your customizations never modify the original browser files.
+13
View File
@@ -46,6 +46,19 @@ For advanced users, Nebula provides tools to manage GPU acceleration.
- **GPU Diagnostics:** View detailed information about your system's GPU and its status. - **GPU Diagnostics:** View detailed information about your system's GPU and its status.
- **GPU Fallback:** If you experience rendering issues, you can apply a GPU fallback to use a more stable rendering path. This can help resolve visual glitches or crashes. - **GPU Fallback:** If you experience rendering issues, you can apply a GPU fallback to use a more stable rendering path. This can help resolve visual glitches or crashes.
### Custom Themes & Customization
Nebula offers extensive customization options to personalize your browsing experience.
- **Theme System:** Choose from built-in themes (default, forest, ocean, sunset) or create your own custom themes.
- **Live Theme Editor:** Modify colors, gradients, and layout options with real-time preview in the settings.
- **Import/Export Themes:** Share custom themes with the community or use themes created by other users.
- **Non-Destructive Design:** All customizations are stored separately and can be reset to default at any time.
- **Layout Options:** Switch between centered, sidebar, and compact view layouts.
- **Custom Branding:** Personalize the browser title and logo visibility.
For detailed information about creating and managing themes, see the [Customization Guide](Customization.md).
### Cross-Platform ### Cross-Platform
Nebula is built with Electron, allowing it to run on multiple operating systems. Nebula is built with Electron, allowing it to run on multiple operating systems.
+455
View File
@@ -0,0 +1,455 @@
/**
* 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'
},
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'
},
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'
},
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'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #744210 0%, #c05621 100%)'
}
};
this.currentTheme = this.loadTheme();
this.init();
}
init() {
this.setupEventListeners();
this.loadCurrentTheme();
this.updatePreview();
}
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;
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
});
});
// Logo options
const showLogoInput = document.getElementById('show-logo');
if (showLogoInput) {
showLogoInput.addEventListener('change', (e) => {
this.currentTheme.showLogo = e.target.checked;
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
});
}
const customTitleInput = document.getElementById('custom-title');
if (customTitleInput) {
customTitleInput.addEventListener('input', (e) => {
this.currentTheme.customTitle = e.target.value || 'Nebula Browser';
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
});
}
// 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%)`;
}
this.saveTheme();
this.updatePreview();
this.applyThemeToPages();
}
}
applyPredefinedTheme(themeName) {
if (this.predefinedThemes[themeName]) {
this.currentTheme = { ...this.predefinedThemes[themeName] };
this.saveTheme();
this.loadCurrentTheme();
this.updatePreview();
this.applyThemeToCurrentPage();
this.applyThemeToPages();
this.updateThemeButtons(themeName);
}
}
updateThemeButtons(activeTheme) {
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.theme === activeTheme) {
btn.classList.add('active');
}
});
}
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');
// Apply colors to preview
previewHome.style.background = this.currentTheme.gradient;
previewLogo.style.color = this.currentTheme.colors.primary;
previewLogo.textContent = this.currentTheme.showLogo ?
`🌌 ${this.currentTheme.customTitle}` : 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);
// 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
// We'll store the theme and let other pages load it
this.saveTheme();
// If we have access to other windows/frames, apply there too
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.saveTheme();
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 };
}
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);
// 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);
+13 -3
View File
@@ -6,6 +6,16 @@
font-display: swap; font-display: swap;
} }
/* CSS Custom Properties for Theming */
:root {
--bg: #121418;
--dark-blue: #0B1C2B;
--dark-purple: #1B1035;
--primary: #7B2EFF;
--accent: #00C6FF;
--text: #E0E0E0;
}
/* Base reset */ /* Base reset */
* { * {
margin: 0; margin: 0;
@@ -13,13 +23,12 @@
box-sizing: border-box; box-sizing: border-box;
} }
body, html { body, html {
/* replace solid bg with a subtle gradient */ /* Use CSS custom properties for theming */
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%; height: 100%;
background: linear-gradient(145deg, #121418 0%, #1B1035 100%); background: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 100%);
color: var(--text); color: var(--text);
overflow: hidden; overflow: hidden;
font-family: 'InterVariable', sans-serif; font-family: 'InterVariable', sans-serif;
@@ -57,6 +66,7 @@ body, html {
.logo-text { .logo-text {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
color: var(--primary);
} }
/* Search bar container */ /* Search bar container */
+37 -4
View File
@@ -73,10 +73,43 @@
</div> </div>
</div> </div>
<!-- make this a module so we can import icons --> <!-- Theme loader script -->
<script type="module" src="home.js"></script> <script src="customization.js"></script>
</body> <script>
</html> // Apply saved theme on page load
document.addEventListener('DOMContentLoaded', () => {
BrowserCustomizer.applyThemeToPage();
// Listen for theme updates from settings
window.addEventListener('message', (event) => {
if (event.data.type === 'theme-update') {
const theme = event.data.theme;
localStorage.setItem('currentTheme', JSON.stringify(theme));
BrowserCustomizer.applyThemeToPage();
// Update logo and title if needed
updateLogoAndTitle(theme);
}
});
// Update logo and title based on theme
function updateLogoAndTitle(theme) {
const logoText = document.querySelector('.logo-text');
const logoImg = document.querySelector('.logo-img');
if (logoText) {
logoText.textContent = theme.customTitle || 'Nebula Browser';
}
if (!theme.showLogo && logoImg) {
logoImg.style.display = 'none';
} else if (logoImg) {
logoImg.style.display = 'block';
}
}
});
</script>
<!-- make this a module so we can import icons --> <!-- make this a module so we can import icons -->
<script type="module" src="home.js"></script> <script type="module" src="home.js"></script>
</body> </body>
+307
View File
@@ -9,9 +9,193 @@
body { font-family: sans-serif; padding: 20px; } body { font-family: sans-serif; padding: 20px; }
section { margin-bottom: 30px; } section { margin-bottom: 30px; }
h2 { border-bottom: 1px solid #ccc; padding-bottom: 5px; } h2 { border-bottom: 1px solid #ccc; padding-bottom: 5px; }
h3 { margin: 15px 0 10px 0; color: var(--accent); font-size: 1.1rem; }
ul { list-style: none; padding-left: 0; } ul { list-style: none; padding-left: 0; }
li { padding: 5px 0; border-bottom: 1px solid #eee; } li { padding: 5px 0; border-bottom: 1px solid #eee; }
.debug-info { background: #f0f0f0; padding: 10px; margin: 10px 0; font-family: monospace; font-size: 12px; } .debug-info { background: #f0f0f0; padding: 10px; margin: 10px 0; font-family: monospace; font-size: 12px; }
/* Customization Styles */
.customization-group {
margin-bottom: 25px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.theme-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.theme-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px;
background: transparent;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text);
}
.theme-btn:hover {
border-color: var(--accent);
}
.theme-btn.active {
border-color: var(--primary);
background: rgba(123, 46, 255, 0.1);
}
.theme-preview {
width: 60px;
height: 40px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.color-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.color-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.color-group label {
font-size: 0.9rem;
color: var(--text);
}
.color-group input[type="color"] {
width: 100%;
height: 40px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: transparent;
cursor: pointer;
}
.layout-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.layout-options label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background 0.2s ease;
}
.layout-options label:hover {
background: rgba(255, 255, 255, 0.05);
}
.logo-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.logo-options label {
display: flex;
align-items: center;
gap: 8px;
}
.logo-options input[type="text"] {
flex: 1;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: var(--text);
}
.theme-management {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.theme-management button {
padding: 8px 16px;
background: var(--primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
}
.theme-management button:hover {
background: var(--accent);
}
.theme-management button:last-child {
background: #e53e3e;
}
.theme-management button:last-child:hover {
background: #c53030;
}
.preview-container {
background: var(--dark-blue);
border-radius: 8px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.preview-home {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--bg);
border-radius: 8px;
min-height: 200px;
}
.preview-logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary);
}
.preview-search {
width: 60%;
height: 40px;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.preview-bookmarks {
display: flex;
gap: 10px;
}
.preview-bookmark {
width: 50px;
height: 50px;
background: var(--accent);
border-radius: 8px;
}
</style> </style>
</head> </head>
<body> <body>
@@ -27,6 +211,123 @@
<div class="debug-info" id="debug-info">Loading debug info...</div> <div class="debug-info" id="debug-info">Loading debug info...</div>
<!-- Customization Section -->
<section>
<h2>🎨 Browser Customization</h2>
<!-- Theme Selection -->
<div class="customization-group">
<h3>Theme Presets</h3>
<div class="theme-selector">
<button id="theme-default" class="theme-btn active" data-theme="default">
<div class="theme-preview" style="background: linear-gradient(145deg, #121418, #1B1035);"></div>
<span>Default</span>
</button>
<button id="theme-ocean" class="theme-btn" data-theme="ocean">
<div class="theme-preview" style="background: linear-gradient(145deg, #1a365d, #2c5282);"></div>
<span>Ocean</span>
</button>
<button id="theme-forest" class="theme-btn" data-theme="forest">
<div class="theme-preview" style="background: linear-gradient(145deg, #1a202c, #2d3748);"></div>
<span>Forest</span>
</button>
<button id="theme-sunset" class="theme-btn" data-theme="sunset">
<div class="theme-preview" style="background: linear-gradient(145deg, #744210, #c05621);"></div>
<span>Sunset</span>
</button>
</div>
</div>
<!-- Color Customization -->
<div class="customization-group">
<h3>Custom Colors</h3>
<div class="color-controls">
<div class="color-group">
<label for="bg-color">Background:</label>
<input type="color" id="bg-color" value="#121418">
</div>
<div class="color-group">
<label for="gradient-color">Gradient End:</label>
<input type="color" id="gradient-color" value="#1B1035">
</div>
<div class="color-group">
<label for="accent-color">Accent:</label>
<input type="color" id="accent-color" value="#7B2EFF">
</div>
<div class="color-group">
<label for="secondary-color">Secondary:</label>
<input type="color" id="secondary-color" value="#00C6FF">
</div>
<div class="color-group">
<label for="text-color">Text:</label>
<input type="color" id="text-color" value="#E0E0E0">
</div>
</div>
</div>
<!-- Home Page Layout -->
<div class="customization-group">
<h3>Home Page Layout</h3>
<div class="layout-options">
<label>
<input type="radio" name="layout" value="centered" checked>
<span>Centered (Default)</span>
</label>
<label>
<input type="radio" name="layout" value="sidebar">
<span>Sidebar Navigation</span>
</label>
<label>
<input type="radio" name="layout" value="compact">
<span>Compact View</span>
</label>
</div>
</div>
<!-- Logo Customization -->
<div class="customization-group">
<h3>Logo & Branding</h3>
<div class="logo-options">
<label for="show-logo">
<input type="checkbox" id="show-logo" checked>
Show Nebula Logo
</label>
<label for="custom-title">
Custom Title:
<input type="text" id="custom-title" placeholder="Nebula Browser" maxlength="50">
</label>
</div>
</div>
<!-- Theme Management -->
<div class="customization-group">
<h3>Theme Management</h3>
<div class="theme-management">
<button id="save-custom-theme">Save Current as Custom Theme</button>
<button id="export-theme">Export Theme</button>
<button id="import-theme">Import Theme</button>
<input type="file" id="theme-file-input" accept=".json" style="display: none;">
<button id="reset-to-default">Reset to Default</button>
</div>
</div>
<!-- Live Preview -->
<div class="customization-group">
<h3>Preview</h3>
<div class="preview-container" id="preview-container">
<div class="preview-home">
<div class="preview-logo">🌌 Nebula</div>
<div class="preview-search"></div>
<div class="preview-bookmarks">
<div class="preview-bookmark"></div>
<div class="preview-bookmark"></div>
<div class="preview-bookmark"></div>
</div>
</div>
</div>
</div>
</section>
<!-- add history views --> <!-- add history views -->
<section> <section>
<h2>Search History</h2> <h2>Search History</h2>
@@ -48,7 +349,13 @@
</div> </div>
<script src="settings.js"></script> <script src="settings.js"></script>
<script src="customization.js"></script>
<script> <script>
// Apply saved theme immediately when page loads
document.addEventListener('DOMContentLoaded', () => {
BrowserCustomizer.applyThemeToPage();
});
// Update debug info // Update debug info
function updateDebugInfo() { function updateDebugInfo() {
const debugDiv = document.getElementById('debug-info'); const debugDiv = document.getElementById('debug-info');
+1
View File
@@ -1,4 +1,5 @@
[ [
"file:///Users/andrewzambazos/Repositories/NebulaBrowser/renderer/index.html",
"https://inscribe.zambazosmedia.group/renderer/editor.html", "https://inscribe.zambazosmedia.group/renderer/editor.html",
"https://inscribe.zambazosmedia.group/", "https://inscribe.zambazosmedia.group/",
"file:///Users/andrewzambazos/Repositories/NebulaBrowser/renderer/index.html", "file:///Users/andrewzambazos/Repositories/NebulaBrowser/renderer/index.html",
+213
View File
@@ -0,0 +1,213 @@
/**
* Theme Manager for Nebula Browser
* Handles theme loading, saving, and management at the application level
*/
const fs = require('fs');
const path = require('path');
class ThemeManager {
constructor() {
this.themesDir = path.join(__dirname, '..', 'themes');
this.userThemesDir = path.join(this.themesDir, 'user');
this.downloadedThemesDir = path.join(this.themesDir, 'downloaded');
this.ensureDirectories();
}
ensureDirectories() {
[this.userThemesDir, this.downloadedThemesDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
}
/**
* Get all available themes
* @returns {Object} Object containing default, user, and downloaded themes
*/
getAllThemes() {
const themes = {
default: this.loadDefaultThemes(),
user: this.loadUserThemes(),
downloaded: this.loadDownloadedThemes()
};
return themes;
}
loadDefaultThemes() {
const defaultThemes = {};
const defaultFiles = ['default.json', 'ocean.json', 'forest.json', 'sunset.json'];
defaultFiles.forEach(file => {
try {
const themePath = path.join(this.themesDir, file);
if (fs.existsSync(themePath)) {
const themeData = JSON.parse(fs.readFileSync(themePath, 'utf8'));
const themeName = path.basename(file, '.json');
defaultThemes[themeName] = themeData;
}
} catch (error) {
console.error(`Error loading default theme ${file}:`, error);
}
});
return defaultThemes;
}
loadUserThemes() {
return this.loadThemesFromDirectory(this.userThemesDir);
}
loadDownloadedThemes() {
return this.loadThemesFromDirectory(this.downloadedThemesDir);
}
loadThemesFromDirectory(directory) {
const themes = {};
try {
if (!fs.existsSync(directory)) {
return themes;
}
const files = fs.readdirSync(directory).filter(file => file.endsWith('.json'));
files.forEach(file => {
try {
const themePath = path.join(directory, file);
const themeData = JSON.parse(fs.readFileSync(themePath, 'utf8'));
const themeName = path.basename(file, '.json');
themes[themeName] = themeData;
} catch (error) {
console.error(`Error loading theme ${file}:`, error);
}
});
} catch (error) {
console.error(`Error reading themes directory ${directory}:`, error);
}
return themes;
}
/**
* Save a user theme
* @param {string} name - Theme name
* @param {Object} themeData - Theme data
* @returns {boolean} Success status
*/
saveUserTheme(name, themeData) {
try {
const filename = name.toLowerCase().replace(/[^a-z0-9]/g, '-') + '.json';
const filepath = path.join(this.userThemesDir, filename);
const themeWithMetadata = {
...themeData,
name: name,
createdAt: new Date().toISOString(),
type: 'user'
};
fs.writeFileSync(filepath, JSON.stringify(themeWithMetadata, null, 2));
return true;
} catch (error) {
console.error('Error saving user theme:', error);
return false;
}
}
/**
* Delete a user theme
* @param {string} filename - Theme filename
* @returns {boolean} Success status
*/
deleteUserTheme(filename) {
try {
const filepath = path.join(this.userThemesDir, filename);
if (fs.existsSync(filepath)) {
fs.unlinkSync(filepath);
return true;
}
return false;
} catch (error) {
console.error('Error deleting user theme:', error);
return false;
}
}
/**
* Import a theme file to downloaded themes
* @param {string} sourceFile - Source file path
* @returns {boolean} Success status
*/
importTheme(sourceFile) {
try {
const themeData = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
// Validate theme structure
if (!this.validateTheme(themeData)) {
throw new Error('Invalid theme structure');
}
const filename = (themeData.name || 'imported-theme')
.toLowerCase()
.replace(/[^a-z0-9]/g, '-') + '.json';
const destinationPath = path.join(this.downloadedThemesDir, filename);
const themeWithMetadata = {
...themeData,
importedAt: new Date().toISOString(),
type: 'downloaded'
};
fs.writeFileSync(destinationPath, JSON.stringify(themeWithMetadata, null, 2));
return true;
} catch (error) {
console.error('Error importing theme:', error);
return false;
}
}
/**
* Validate theme structure
* @param {Object} theme - Theme object to validate
* @returns {boolean} Is valid
*/
validateTheme(theme) {
return theme &&
theme.colors &&
theme.colors.bg &&
theme.colors.primary &&
theme.colors.accent &&
theme.colors.text &&
typeof theme.showLogo === 'boolean' &&
theme.layout &&
theme.customTitle;
}
/**
* Get theme by name and type
* @param {string} name - Theme name
* @param {string} type - Theme type (default, user, downloaded)
* @returns {Object|null} Theme data or null
*/
getTheme(name, type = 'default') {
const themes = this.getAllThemes();
return themes[type] && themes[type][name] ? themes[type][name] : null;
}
/**
* Apply theme to application (for use in main process)
* @param {Object} theme - Theme to apply
*/
applyThemeToApp(theme) {
// This would be used to apply themes at the application level
// For example, updating window chrome colors, etc.
console.log('Applying theme to application:', theme.name);
}
}
module.exports = ThemeManager;
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Default Nebula",
"colors": {
"bg": "#121418",
"darkBlue": "#0B1C2B",
"darkPurple": "#1B1035",
"primary": "#7B2EFF",
"accent": "#00C6FF",
"text": "#E0E0E0"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #121418 0%, #1B1035 100%)",
"version": "1.0",
"description": "The original Nebula Browser theme with purple and blue gradients"
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Forest Night",
"colors": {
"bg": "#1a202c",
"darkBlue": "#2d3748",
"darkPurple": "#4a5568",
"primary": "#68d391",
"accent": "#9ae6b4",
"text": "#f7fafc"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #1a202c 0%, #2d3748 100%)",
"version": "1.0",
"description": "A nature-inspired theme with forest greens and earth tones"
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Ocean Depths",
"colors": {
"bg": "#1a365d",
"darkBlue": "#2a4365",
"darkPurple": "#2c5282",
"primary": "#3182ce",
"accent": "#00d9ff",
"text": "#e2e8f0"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #1a365d 0%, #2c5282 100%)",
"version": "1.0",
"description": "A calming ocean-inspired theme with blues and teals"
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Sunset Glow",
"colors": {
"bg": "#744210",
"darkBlue": "#975a16",
"darkPurple": "#c05621",
"primary": "#ed8936",
"accent": "#fbb040",
"text": "#fffaf0"
},
"layout": "centered",
"showLogo": true,
"customTitle": "Nebula Browser",
"gradient": "linear-gradient(145deg, #744210 0%, #c05621 100%)",
"version": "1.0",
"description": "A warm sunset theme with oranges and golden hues"
}