10180b7109
Introduce support for an internal nebula:// URL scheme and internal page routing (ResolveInternalUrl / ToInternalUrl), including dedicated slugs for home, settings, downloads, big-picture, gpu-diagnostics, insecure and a 404 fallback. Wire internal resolution into browser creation and tab navigation so internal pages load from local UI files. Add an insecure-warning interstitial flow with a navigate-insecure command and a one-shot bypass set (ShouldBypassInsecureWarning) so content can request navigating to an HTTP target after user confirmation. Harden BrowserClient handling to resolve Chromium new-tab and nebula internal URLs, redirect HTTP to the insecure warning when appropriate, and handle 404 responses by loading the internal 404 page. Update chrome UI behavior to hide internal home URLs, accept nebula:// in navigation input checks, and add a GPU Diagnostics page (revamped UI + diagnostic scripts) plus menu entry. Misc: improve URL utilities (scheme checks, percent-encoding, decorations), fix 404 display text, adjust menu popup size, tweak window frame styling (DWM attributes) and remove branding block from chrome UI CSS.
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:|nebula:\/\/)/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);
|
|
});
|