Implement custom Windows title bar and theme refactor

Adds a frameless window with custom title bar controls for Windows, including minimize, maximize/restore, and close buttons. Refactors theme color application to use CSS variables and color-mix for improved consistency and dynamic theming across home, settings, and main UI. Updates renderer and styles to support the new title bar, platform detection, and improved color handling. No changes to lock files.
This commit is contained in:
2026-01-01 15:25:45 +13:00
parent 1b71a4001d
commit 26126982e2
6 changed files with 398 additions and 188 deletions
+185 -19
View File
@@ -128,25 +128,111 @@ function addToSiteHistory(url) {
}
}
// Store current theme colors globally for use by renderTabs
let currentThemeColors = null;
// Apply theme colors to the main UI (URL bar and tabs)
function applyThemeToMainUI(theme) {
if (!theme || !theme.colors) return;
const root = document.documentElement;
const colors = theme.colors;
// Apply URL bar and tab colors
if (theme.colors.urlBarBg) root.style.setProperty('--url-bar-bg', theme.colors.urlBarBg);
if (theme.colors.urlBarText) root.style.setProperty('--url-bar-text', theme.colors.urlBarText);
if (theme.colors.urlBarBorder) root.style.setProperty('--url-bar-border', theme.colors.urlBarBorder);
if (theme.colors.tabBg) root.style.setProperty('--tab-bg', theme.colors.tabBg);
if (theme.colors.tabText) root.style.setProperty('--tab-text', theme.colors.tabText);
if (theme.colors.tabActive) root.style.setProperty('--tab-active', theme.colors.tabActive);
if (theme.colors.tabActiveText) root.style.setProperty('--tab-active-text', theme.colors.tabActiveText);
if (theme.colors.tabBorder) root.style.setProperty('--tab-border', theme.colors.tabBorder);
// Store colors globally for renderTabs to use
currentThemeColors = colors;
// Set CSS variables on root for elements using var()
const setVar = (cssVar, value, fallback) => {
const val = value || fallback;
if (val) root.style.setProperty(cssVar, val);
};
// Core palette so popups/menus and the address bar stay in sync
setVar('--bg', colors.bg, '#0b0d10');
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');
// URL bar + tab strip styling
setVar('--url-bar-bg', colors.urlBarBg, '#1c2030');
setVar('--url-bar-text', colors.urlBarText, '#e0e0e0');
setVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
setVar('--tab-bg', colors.tabBg, '#161925');
setVar('--tab-text', colors.tabText, '#a4a7b3');
setVar('--tab-active', colors.tabActive, '#1c2030');
setVar('--tab-active-text', colors.tabActiveText, '#e0e0e0');
setVar('--tab-border', colors.tabBorder, '#2b3040');
// Also directly apply to key elements to ensure styles take effect
const nav = document.getElementById('nav');
const titlebarContainer = document.getElementById('titlebar-container');
const tabBar = document.getElementById('tab-bar');
const urlBox = document.getElementById('url');
const navCenter = document.querySelector('.nav-center');
console.log('[THEME] Applied theme colors to main UI');
if (nav) {
nav.style.setProperty('background', colors.urlBarBg || '#1c2030', 'important');
nav.style.setProperty('border-bottom-color', colors.urlBarBorder || '#3e4652', 'important');
}
if (navCenter) {
navCenter.style.setProperty('background', colors.urlBarBg || '#1c2030', 'important');
navCenter.style.setProperty('border-color', colors.urlBarBorder || '#3e4652', 'important');
}
if (titlebarContainer) {
titlebarContainer.style.setProperty('background', colors.tabBg || '#161925', 'important');
}
if (tabBar) {
tabBar.style.setProperty('background', colors.tabBg || '#161925', 'important');
tabBar.style.setProperty('border-bottom-color', colors.tabBorder || '#2b3040', 'important');
}
if (urlBox) {
urlBox.style.setProperty('color', colors.urlBarText || '#e0e0e0', 'important');
}
// Update existing tab elements to reflect new theme colors
document.querySelectorAll('.tab').forEach(tab => {
const isActive = tab.classList.contains('active');
tab.style.setProperty('background', isActive
? (colors.tabActive || '#1c2030')
: (colors.tabBg || '#161925'), 'important');
tab.style.setProperty('color', isActive
? (colors.tabActiveText || '#e0e0e0')
: (colors.tabText || '#a4a7b3'), 'important');
tab.style.setProperty('border-color', colors.tabBorder || '#2b3040', 'important');
});
// Align the chrome background with the theme gradient or fallback
if (theme.gradient) {
document.body.style.background = theme.gradient;
} else if (colors.bg) {
document.body.style.background = colors.bg;
}
// Persist so other pages (home/settings) can pull the latest palette
try { localStorage.setItem('currentTheme', JSON.stringify(theme)); } catch {}
console.log('[THEME] Applied theme to main UI:', {
urlBarBg: colors.urlBarBg,
tabBg: colors.tabBg,
navFound: !!nav,
titlebarFound: !!titlebarContainer,
tabBarFound: !!tabBar
});
}
// Detect platform and add class to body for CSS platform-specific styling
(function detectPlatform() {
const platform = navigator.platform.toLowerCase();
if (platform.includes('mac')) {
document.body.classList.add('platform-darwin');
} else if (platform.includes('win')) {
document.body.classList.add('platform-win32');
} else {
document.body.classList.add('platform-linux');
}
})();
// 1) cache hot DOM references
const urlBox = document.getElementById('url');
const tabBarEl = document.getElementById('tab-bar');
@@ -729,6 +815,8 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) {
// After creating dynamic webview:
webview.addEventListener('ipc-message', e => {
if (e.channel === 'theme-update') {
const theme = e.args && e.args[0];
if (theme) applyThemeToMainUI(theme);
const home = document.getElementById('home-webview');
if (home) home.send('theme-update', ...e.args);
} else if (e.channel === 'navigate' && e.args[0]) {
@@ -907,6 +995,18 @@ function renderTabs() {
el.setAttribute('tabindex', tab.id === activeTabId ? '0' : '-1');
el.dataset.tabId = tab.id;
currentOrder.push(tab.id);
// Apply theme colors to new tab element
if (currentThemeColors) {
const isActive = tab.id === activeTabId;
el.style.setProperty('background', isActive
? (currentThemeColors.tabActive || '#1c2030')
: (currentThemeColors.tabBg || '#161925'), 'important');
el.style.setProperty('color', isActive
? (currentThemeColors.tabActiveText || '#e0e0e0')
: (currentThemeColors.tabText || '#a4a7b3'), 'important');
el.style.setProperty('border-color', currentThemeColors.tabBorder || '#2b3040', 'important');
}
if (!lastTabOrder.includes(tab.id)) {
// New tab enters with animation
@@ -1215,6 +1315,18 @@ window.addEventListener('DOMContentLoaded', () => {
try {
const theme = JSON.parse(savedTheme);
applyThemeToMainUI(theme);
// Also send to home-webview once it's ready
const homeWebview = document.getElementById('home-webview');
if (homeWebview) {
const sendThemeToHome = () => {
try { homeWebview.send('theme-update', theme); } catch {}
};
// If already loaded, send immediately; otherwise wait for dom-ready
if (homeWebview.getWebContentsId) {
sendThemeToHome();
}
homeWebview.addEventListener('dom-ready', sendThemeToHome, { once: true });
}
} catch (err) {
console.error('Error applying saved theme:', err);
}
@@ -1371,17 +1483,71 @@ window.addEventListener('DOMContentLoaded', () => {
window.downloadsAPI?.onDone(()=> { refreshDownloadsMini(); });
window.downloadsAPI?.onCleared(()=> { refreshDownloadsMini(); });
// window control bindings
// window control bindings (Windows frameless window)
const minBtn = document.getElementById('min-btn');
const maxBtn = document.getElementById('max-btn');
const closeBtn = document.getElementById('close-btn');
if (minBtn && maxBtn && closeBtn) {
if (process.platform !== 'darwin') {
minBtn.addEventListener('click', () => ipcRenderer.invoke('window-minimize'));
maxBtn.addEventListener('click', () => ipcRenderer.invoke('window-maximize'));
closeBtn.addEventListener('click', () => ipcRenderer.invoke('window-close'));
} else {
document.getElementById('window-controls').style.display = 'none';
const windowControls = document.getElementById('window-controls');
console.log('[WindowControls] Elements found:', { minBtn: !!minBtn, maxBtn: !!maxBtn, closeBtn: !!closeBtn, windowControls: !!windowControls });
// Detect platform - hide controls on macOS (uses native traffic lights)
const isMacOS = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
console.log('[WindowControls] Platform:', navigator.platform, 'isMacOS:', isMacOS);
if (windowControls) {
if (isMacOS) {
// Hide window controls on macOS
windowControls.style.display = 'none';
// Remove right padding for window controls
document.getElementById('tab-bar').style.paddingRight = '10px';
} else if (minBtn && maxBtn && closeBtn) {
// Windows/Linux: Set up custom title bar controls
console.log('[WindowControls] Setting up event listeners for Windows/Linux');
minBtn.addEventListener('click', (e) => {
console.log('[WindowControls] Minimize clicked');
e.stopPropagation();
ipcRenderer.invoke('window-minimize');
});
maxBtn.addEventListener('click', async (e) => {
console.log('[WindowControls] Maximize clicked');
e.stopPropagation();
await ipcRenderer.invoke('window-maximize');
updateMaximizeIcon();
});
closeBtn.addEventListener('click', (e) => {
console.log('[WindowControls] Close clicked');
e.stopPropagation();
ipcRenderer.invoke('window-close');
});
// Update maximize icon based on window state
async function updateMaximizeIcon() {
try {
const isMaximized = await ipcRenderer.invoke('window-is-maximized');
const maximizeIcon = maxBtn.querySelector('.maximize-icon');
const restoreIcon = maxBtn.querySelector('.restore-icon');
if (maximizeIcon && restoreIcon) {
maximizeIcon.style.display = isMaximized ? 'none' : 'block';
restoreIcon.style.display = isMaximized ? 'block' : 'none';
maxBtn.title = isMaximized ? 'Restore' : 'Maximize';
maxBtn.setAttribute('aria-label', isMaximized ? 'Restore' : 'Maximize');
}
} catch (e) {
// Ignore errors during state check
}
}
// Initial state check
updateMaximizeIcon();
// Listen for window resize to update maximize icon
window.addEventListener('resize', () => {
// Debounce resize events
clearTimeout(window._maximizeIconTimeout);
window._maximizeIconTimeout = setTimeout(updateMaximizeIcon, 100);
});
}
}