a8786b4c1c
Introduce core application structure and browser management: add NebulaController and run entry (src/app/*) to centralize window, tab and CEF lifecycle logic; implement TabManager and NebulaTab (src/browser/*) for tab creation, navigation and state tracking; add URL utilities (NormalizeNavigationInput, JsonEscape) and CEF browser client glue (src/cef/browser_client.cpp/.h) to forward chrome commands and content events. Update app/main.cpp to delegate startup to nebula::app::RunNebula. Add UI assets (chrome.html, chrome.css, chrome.js, lucide, menu-popup updates) and remove obsolete nebot.html. Update CMakeLists to include new sources, add ${CMAKE_SOURCE_DIR}/src to includes and link dwmapi on Windows. Overall this refactors startup and splits responsibilities for cleaner tab and browser lifecycle handling.
183 lines
5.5 KiB
JavaScript
183 lines
5.5 KiB
JavaScript
const SEARCH_URL = 'https://www.google.com/search?q=';
|
|
|
|
const state = {
|
|
id: 1,
|
|
url: '',
|
|
title: 'New Tab',
|
|
isLoading: false,
|
|
progress: 0,
|
|
canGoBack: false,
|
|
canGoForward: false,
|
|
favicon: '',
|
|
tabs: []
|
|
};
|
|
|
|
function toNavigationUrl(input) {
|
|
const value = (input || '').trim();
|
|
if (!value) return null;
|
|
if (/^(https?:|file:|data:|blob:|chrome:)/i.test(value)) return value;
|
|
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
|
return `${SEARCH_URL}${encodeURIComponent(value)}`;
|
|
}
|
|
|
|
function postCommand(command, payload = '') {
|
|
if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
|
|
window.nebulaNative.postMessage(command, String(payload));
|
|
}
|
|
}
|
|
|
|
function renderFavicon(favicon, tab) {
|
|
const url = (tab.favicon || '').trim();
|
|
favicon.className = 'tab-favicon';
|
|
favicon.textContent = '';
|
|
|
|
if (!url) {
|
|
favicon.classList.add('empty');
|
|
return;
|
|
}
|
|
|
|
const image = document.createElement('img');
|
|
image.alt = '';
|
|
image.decoding = 'async';
|
|
image.draggable = false;
|
|
image.addEventListener('load', () => {
|
|
favicon.classList.add('has-favicon');
|
|
});
|
|
image.addEventListener('error', () => {
|
|
image.remove();
|
|
favicon.classList.remove('has-favicon');
|
|
favicon.classList.add('empty');
|
|
});
|
|
|
|
favicon.append(image);
|
|
image.src = url;
|
|
}
|
|
|
|
function renderTabs() {
|
|
const tabsElement = document.querySelector('.tabs');
|
|
const addButton = tabsElement.querySelector('.tab-add');
|
|
const tabs = state.tabs.length
|
|
? state.tabs
|
|
: [{ id: state.id, title: state.title, isLoading: state.isLoading, favicon: state.favicon }];
|
|
|
|
tabsElement.querySelectorAll('.tab').forEach(tab => tab.remove());
|
|
|
|
tabs.forEach(tab => {
|
|
const button = document.createElement('div');
|
|
const isActive = tab.id === state.id;
|
|
button.className = `tab${isActive ? ' active' : ''}`;
|
|
button.setAttribute('role', 'tab');
|
|
button.setAttribute('aria-selected', String(isActive));
|
|
button.tabIndex = 0;
|
|
button.dataset.tabId = String(tab.id);
|
|
|
|
const favicon = document.createElement('span');
|
|
renderFavicon(favicon, tab);
|
|
|
|
const title = document.createElement('span');
|
|
title.className = 'tab-title';
|
|
title.textContent = tab.title || 'New Tab';
|
|
|
|
const loading = document.createElement('span');
|
|
loading.className = 'tab-loading';
|
|
loading.hidden = !tab.isLoading;
|
|
|
|
const close = document.createElement('button');
|
|
close.className = 'tab-close';
|
|
close.type = 'button';
|
|
close.title = 'Close tab';
|
|
close.setAttribute('aria-label', `Close ${tab.title || 'New Tab'}`);
|
|
close.dataset.tabId = String(tab.id);
|
|
close.innerHTML = '<i data-lucide="x"></i>';
|
|
|
|
button.append(favicon, title, loading, close);
|
|
tabsElement.insertBefore(button, addButton);
|
|
});
|
|
|
|
if (window.lucide) lucide.createIcons({ nodes: [tabsElement] });
|
|
}
|
|
|
|
function applyState(nextState) {
|
|
Object.assign(state, nextState || {});
|
|
|
|
const title = state.title || 'New Tab';
|
|
const url = state.url || '';
|
|
const addressInput = document.getElementById('address-input');
|
|
const backButton = document.getElementById('back-button');
|
|
const forwardButton = document.getElementById('forward-button');
|
|
const reloadButton = document.getElementById('reload-button');
|
|
const progressBar = document.getElementById('progress-bar');
|
|
|
|
document.title = `${title} - Nebula`;
|
|
renderTabs();
|
|
backButton.disabled = !state.canGoBack;
|
|
forwardButton.disabled = !state.canGoForward;
|
|
const reloadIcon = state.isLoading ? 'x' : 'rotate-cw';
|
|
reloadButton.dataset.command = state.isLoading ? 'stop' : 'reload';
|
|
reloadButton.innerHTML = `<i data-lucide="${reloadIcon}"></i>`;
|
|
if (window.lucide) lucide.createIcons({ nodes: [reloadButton] });
|
|
|
|
if (document.activeElement !== addressInput) {
|
|
addressInput.value = url;
|
|
}
|
|
|
|
progressBar.style.width = `${Math.max(0, Math.min(1, state.progress || 0)) * 100}%`;
|
|
progressBar.style.opacity = state.isLoading ? '1' : '0';
|
|
}
|
|
|
|
function wireCommands() {
|
|
document.querySelector('.tabs').addEventListener('click', event => {
|
|
const close = event.target.closest('.tab-close[data-tab-id]');
|
|
if (close) {
|
|
postCommand('close-tab', close.dataset.tabId);
|
|
return;
|
|
}
|
|
|
|
const tab = event.target.closest('.tab[data-tab-id]');
|
|
if (tab && !tab.classList.contains('active')) {
|
|
postCommand('activate-tab', tab.dataset.tabId);
|
|
}
|
|
});
|
|
|
|
document.querySelector('.tabs').addEventListener('auxclick', event => {
|
|
if (event.button !== 1) return;
|
|
|
|
const tab = event.target.closest('.tab[data-tab-id]');
|
|
if (tab) {
|
|
postCommand('close-tab', tab.dataset.tabId);
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('[data-command]').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
postCommand(button.dataset.command);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-drag-region]').forEach(region => {
|
|
region.addEventListener('pointerdown', event => {
|
|
const interactive = event.target.closest('button, input, .tab, .address-shell');
|
|
if (event.button === 0 && !interactive) {
|
|
postCommand('drag');
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('address-form').addEventListener('submit', event => {
|
|
event.preventDefault();
|
|
const input = document.getElementById('address-input');
|
|
const target = toNavigationUrl(input.value);
|
|
if (target) {
|
|
postCommand('navigate', target);
|
|
input.blur();
|
|
}
|
|
});
|
|
}
|
|
|
|
window.NebulaChrome = { applyState, postCommand, toNavigationUrl };
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
wireCommands();
|
|
applyState(state);
|
|
});
|