Neb added

This commit is contained in:
2025-07-25 22:46:38 +12:00
parent 1b6c58a348
commit cfd2ccf50d
22 changed files with 4483 additions and 223 deletions
+1
View File
@@ -0,0 +1 @@
[]
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sleek Electron Browser</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="navbar">
<button id="back-button" title="Go Back"></button>
<button id="forward-button" title="Go Forward"></button>
<button id="refresh-button" title="Refresh Page"></button>
<input type="text" id="address-bar" placeholder="Enter URL" value="https://www.google.com/">
<button id="go-button" title="Go">Go</button>
</div>
<script src="renderer.js"></script>
</body>
</html>
+249 -70
View File
@@ -1,92 +1,271 @@
const { app, BrowserWindow, BrowserView, ipcMain } = require('electron'); // Add ipcMain here
const path = require('node:path');
const { app, BrowserWindow, ipcMain, session, screen, shell } = require('electron');
const fs = require('fs');
const path = require('path');
let mainWindow;
let browserView;
app.commandLine.appendSwitch('ignore-gpu-blacklist');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers');
app.commandLine.appendSwitch('ignore-gpu-blocklist');
app.commandLine.appendSwitch('enable-accelerated-video-decode');
app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder,CanvasOopRasterization');
app.commandLine.appendSwitch('no-sandbox'); // Optional, for some setups
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
titleBarStyle: 'hiddenInset',
// Set a custom application name
app.setName('Nebula');
// --- clear any prior registrations to prevent duplicatehandler errors ---
ipcMain.removeHandler('window-minimize');
ipcMain.removeHandler('window-maximize');
ipcMain.removeHandler('window-close');
function createWindow(startUrl) {
// Get the available screen size
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
// Ensure nativeWindowOpen is disabled
let windowOptions = {
width,
height,
resizable: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
webviewTag: false
}
});
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools();
browserView = new BrowserView({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
}
});
mainWindow.setBrowserView(browserView);
const navBarHeight = 80; // Make sure this matches your CSS navbar height
const updateBrowserViewBounds = () => {
const { width, height } = mainWindow.getBounds();
browserView.setBounds({ x: 0, y: navBarHeight, width: width, height: height - navBarHeight });
nodeIntegration: true,
contextIsolation: true, // was false
webviewTag: true,
enableRemoteModule: true, // Enable the remote module
nodeIntegrationInSubFrames: true, // ← allow require() inside your <webview>
nativeWindowOpen: false // Prevent Electron from creating new windows
},
fullscreen: false,
autoHideMenuBar: true,
icon: path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'),
title: 'Nebula',
};
updateBrowserViewBounds(); // Set initial bounds
browserView.webContents.loadURL('https://www.google.com');
mainWindow.on('resize', updateBrowserViewBounds);
// --- IPC Handlers ---
ipcMain.on('load-url', (event, url) => {
let formattedUrl = url;
// Basic URL formatting: if no protocol, assume https://
if (!formattedUrl.match(/^[a-zA-Z]+:\/\//)) {
formattedUrl = 'https://' + formattedUrl;
if (process.platform === 'darwin') {
Object.assign(windowOptions, {
frame: true,
titleBarStyle: 'hidden',
trafficLightPosition: { x: 15, y: 20 },
backgroundColor: '#00000000',
transparent: true,
});
} else if (process.platform === 'win32') {
Object.assign(windowOptions, {
frame: true, // Use default Windows title bar.
// removed titleBarOverlay to restore native Windows controls.
});
} else {
windowOptions.frame = true;
}
browserView.webContents.loadURL(formattedUrl).catch(err => {
console.error('Failed to load URL:', err);
// Optionally send an error back to the renderer
const win = new BrowserWindow(windowOptions);
// Handle window.open() calls load URL in this window
win.webContents.setWindowOpenHandler(({ url }) => {
win.loadURL(url);
return { action: 'deny' };
});
// Intercept direct navigations (e.g., user clicks a link) load URL in this window
win.webContents.on('will-navigate', (event, url) => {
event.preventDefault(); // Prevent navigation in the current window
win.loadURL(url);
});
// Intercept legacy new-window events load URL in this window
win.webContents.on('new-window', (event, url) => {
event.preventDefault(); // Prevent new Electron window
win.loadURL(url);
});
// ensure all embedded <webview> tags also use the same window
win.webContents.on('did-attach-webview', (event, webContents) => {
// intercept window.open() inside webview
webContents.setWindowOpenHandler(({ url }) => {
webContents.loadURL(url);
return { action: 'deny' };
});
// intercept legacy new-window on webview
webContents.on('new-window', (e, url) => {
e.preventDefault();
webContents.loadURL(url);
});
// intercept navigation on webview (e.g. user clicks link)
webContents.on('will-navigate', (e, url) => {
e.preventDefault();
webContents.loadURL(url);
});
});
ipcMain.on('go-back', () => {
if (browserView.webContents.canGoBack()) {
browserView.webContents.goBack();
win.loadFile('renderer/index.html');
// if caller passed in a URL, forward it to the renderer after load
if (startUrl) {
win.webContents.once('did-finish-load', () => {
win.webContents.send('open-url', startUrl);
});
}
// Set default zoom to 100%
const zoomFactor = 1.0;
win.webContents.on('did-finish-load', () => {
win.webContents.setZoomFactor(zoomFactor);
});
// record site and search history on every navigation
const recordHistory = async (fileName, entry) => {
const filePath = path.join(__dirname, fileName);
let data = [];
try { data = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch {}
if (data[0] !== entry) {
data.unshift(entry);
if (data.length > 100) data.pop();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
};
win.webContents.on('did-navigate', (event, url) => {
recordHistory('site-history.json', url);
const m = /[?&](?:q|query)=([^&]+)/.exec(url);
if (m && m[1]) {
const query = decodeURIComponent(m[1].replace(/\+/g, ' '));
recordHistory('search-history.json', query);
}
});
}
ipcMain.on('go-forward', () => {
if (browserView.webContents.canGoForward()) {
browserView.webContents.goForward();
}
});
ipcMain.on('refresh-page', () => {
browserView.webContents.reload();
});
// --- End IPC Handlers ---
};
// This method will be called when Electron has finished initialization
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
if (process.platform === 'darwin') {
// Set macOS dock icon using an icns file for proper display.
app.dock.setIcon(path.join(__dirname, 'assets/images/Logos/Nebula-Icon.icns'));
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Quit when all windows are closed.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
if (process.platform !== 'darwin') app.quit();
});
// ipcMain handlers
// --- window control handlers (only registered once now)
ipcMain.handle('window-minimize', event => {
BrowserWindow.fromWebContents(event.sender).minimize();
});
ipcMain.handle('window-maximize', event => {
const w = BrowserWindow.fromWebContents(event.sender);
w.isMaximized() ? w.unmaximize() : w.maximize();
});
ipcMain.handle('window-close', event => {
BrowserWindow.fromWebContents(event.sender).close();
});
// Add site and search history IPC handlers
ipcMain.handle('load-site-history', async () => {
const filePath = path.join(__dirname, 'site-history.json');
try {
const data = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(data);
} catch (err) {
return [];
}
});
ipcMain.handle('save-site-history', async (event, history) => {
const filePath = path.join(__dirname, 'site-history.json');
try {
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
return true;
} catch (err) {
return false;
}
});
ipcMain.handle('load-search-history', async () => {
const filePath = path.join(__dirname, 'search-history.json');
try {
const data = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(data);
} catch (err) {
return [];
}
});
ipcMain.handle('save-search-history', async (event, history) => {
const filePath = path.join(__dirname, 'search-history.json');
try {
fs.writeFileSync(filePath, JSON.stringify(history, null, 2));
return true;
} catch (err) {
return false;
}
});
// debug: log defaulthomepage changes from renderer
ipcMain.on('homepage-changed', (event, url) => {
console.log('[MAIN] homepage-changed →', url);
});
ipcMain.handle('clear-browser-data', async () => {
try {
const ses = session.defaultSession;
// Clear cookies
await ses.clearStorageData({ storages: ['cookies'] });
// Clear local storage and other storage data
await ses.clearStorageData({ storages: ['localstorage', 'indexdb', 'filesystem', 'websql'] });
// Clear cache
await ses.clearCache();
// Clear HTTP authentication cache
await ses.clearAuthCache();
// Clear all cookies explicitly to ensure logged-in accounts are logged out
const cookies = await ses.cookies.get({});
for (const cookie of cookies) {
await ses.cookies.remove(cookie.url, cookie.name);
}
return true; // Indicate success
} catch (error) {
console.error('Failed to clear browser data:', error);
return false; // Indicate failure
}
});
ipcMain.handle('get-zoom-factor', event => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents;
return wc.getZoomFactor();
});
ipcMain.handle('zoom-in', event => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents;
const current = wc.getZoomFactor();
const z = Math.min(current + 0.1, 3);
wc.setZoomFactor(z);
return z;
});
ipcMain.handle('zoom-out', event => {
const wc = BrowserWindow.fromWebContents(event.sender).webContents;
const current = wc.getZoomFactor();
const z = Math.max(current - 0.1, 0.25);
wc.setZoomFactor(z);
return z;
});
// allow renderer to pop a tab into its own window
ipcMain.handle('open-tab-in-new-window', (event, url) => {
createWindow(url);
});
+2401 -12
View File
File diff suppressed because it is too large Load Diff
+14 -13
View File
@@ -1,24 +1,25 @@
{
"name": "nebulabrowser",
"name": "nebula",
"productName": "Nebula",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start" : "electron . --disable-gpu-sandbox"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Bobbybear007/NebulaBrowser.git"
"start": "electron .",
"dist": "electron-builder"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Bobbybear007/NebulaBrowser/issues"
},
"homepage": "https://github.com/Bobbybear007/NebulaBrowser#readme",
"description": "",
"devDependencies": {
"electron": "^37.2.4"
"electron": "^37.2.3",
"electron-builder": "^23.0.0"
},
"build": {
"appId": "com.andrewzambazos.nebula",
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/images/Logos/Nebula-Icon.ico"
}
}
}
+15 -7
View File
@@ -1,10 +1,18 @@
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// Expose a limited set of IPC methods to the renderer process
contextBridge.exposeInMainWorld('electronAPI', {
loadURL: (url) => ipcRenderer.send('load-url', url),
goBack: () => ipcRenderer.send('go-back'),
goForward: () => ipcRenderer.send('go-forward'),
refreshPage: () => ipcRenderer.send('refresh-page'),
// You can add more APIs here as your browser grows
window.addEventListener('DOMContentLoaded', () => {
console.log("Browser UI loaded.");
});
// stubbed preload—no-op or expose an API as needed
contextBridge.exposeInMainWorld('electronAPI', {
send: (ch, ...args) => ipcRenderer.send(ch, ...args),
invoke: (ch, ...args) => ipcRenderer.invoke(ch, ...args),
on: (ch, fn) => ipcRenderer.on(ch, (e, ...args) => fn(...args))
});
contextBridge.exposeInMainWorld('bookmarksAPI', {
load: () => ipcRenderer.invoke('load-bookmarks'),
save: (data) => ipcRenderer.invoke('save-bookmarks', data)
});
-46
View File
@@ -1,46 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const addressBar = document.getElementById('address-bar');
const goButton = document.getElementById('go-button');
const backButton = document.getElementById('back-button');
const forwardButton = document.getElementById('forward-button');
const refreshButton = document.getElementById('refresh-button');
// Load initial URL if present in address bar
const initialUrl = addressBar.value;
if (initialUrl) {
window.electronAPI.loadURL(initialUrl);
}
goButton.addEventListener('click', () => {
const url = addressBar.value;
if (url) {
window.electronAPI.loadURL(url);
}
});
addressBar.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
const url = addressBar.value;
if (url) {
window.electronAPI.loadURL(url);
}
}
});
backButton.addEventListener('click', () => {
window.electronAPI.goBack();
});
forwardButton.addEventListener('click', () => {
window.electronAPI.goForward();
});
refreshButton.addEventListener('click', () => {
window.electronAPI.refreshPage();
});
// You can add more logic here, e.g., to update the address bar
// when the BrowserView navigates to a new URL. This requires
// IPC communication from the main process to the renderer.
// We'll leave that as a potential enhancement for later.
});
+81
View File
@@ -0,0 +1,81 @@
/* Load InterVariable */
@font-face {
font-family: 'InterVariable';
src: url('../assets/images/fonts/InterVariable.ttf') format('truetype');
font-weight: 100 900;
font-display: swap;
}
:root {
--bg: #121418;
--dark-blue: #0B1C2B;
--dark-purple: #1B1035;
--primary: #7B2EFF;
--accent: #00C6FF;
--text: #E0E0E0;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: 'InterVariable', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
background-color: var(--dark-purple);
padding: 2rem;
border-radius: 20px;
box-shadow: 0 0 12px rgba(0, 0, 0, 0.4);
width: 90%;
max-width: 500px;
}
.error-icon {
font-size: 4rem;
margin-bottom: 1rem;
color: var(--accent);
}
h1 {
margin: 0;
font-size: 1.8rem;
color: var(--primary);
}
p {
margin: 0.5rem 0;
color: var(--text);
}
.url-line {
font-style: italic;
color: var(--text);
}
.actions {
margin-top: 1.5rem;
display: flex;
justify-content: center;
gap: 1rem;
}
button {
padding: 0.6rem 1.2rem;
background-color: var(--primary);
color: var(--text);
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background-color: var(--accent);
}
+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 - Lost in the Void</title>
<link rel="stylesheet" href="404.css">
</head>
<body>
<div class="container">
<div class="error-icon">🪐</div>
<h1>404 - Page Not Found</h1>
<p>Youve warped into an unknown sector.</p>
<p class="url-line">Tried to reach: <span id="attempted-url"></span></p>
<div class="actions">
<button onclick="goHome()">Go to Home</button>
<button onclick="goSettings()">Open Settings</button>
</div>
</div>
<script>
function getQueryParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
const attemptedUrl = getQueryParam("url");
if (attemptedUrl) {
document.getElementById("attempted-url").textContent = decodeURIComponent(attemptedUrl);
}
function goHome() {
window.location.href = "https://google.com";
}
function goSettings() {
window.location.href = "settings.html";
}
</script>
</body>
</html>
+281
View File
@@ -0,0 +1,281 @@
/* Load InterVariable */
@font-face {
font-family: 'InterVariable';
src: url('../assets/images/fonts/InterVariable.ttf') format('truetype');
font-weight: 100 900;
font-display: swap;
}
/* Base reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, html {
/* replace solid bg with a subtle gradient */
margin: 0;
padding: 0;
height: 100%;
background: linear-gradient(145deg, #121418 0%, #1B1035 100%);
color: var(--text);
overflow: hidden;
font-family: 'InterVariable', sans-serif;
}
/* Center everything */
.home-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* Center content vertically */
height: 100vh;
overflow-y: auto;
text-align: center;
padding: 2rem;
}
/* Logo block */
.logo {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 2rem;
}
.logo-img {
/* bump up logo size and add subtle shadow */
width: 150px;
height: 150px;
margin-bottom: 1rem;
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.5));
}
.logo-text {
font-size: 2rem;
font-weight: bold;
}
/* Search bar */
.search-bar {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 70px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
padding: 0.25rem;
margin-bottom: 2rem;
width: 500px;
max-width: 90vw;
overflow: hidden;
}
.search-bar input.search-input {
flex: 1;
border: none;
background: transparent;
padding: 0.75rem 1rem;
font-size: 1rem;
color: #333;
}
.search-bar button.search-btn {
border: none;
background: linear-gradient(90deg, var(--primary), var(--accent));
color: white;
padding: 0.75rem;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
}
.search-bar button.search-btn:hover {
transform: scale(1.1);
}
.search-bar button.search-btn .material-symbols-outlined {
font-size: 1.25rem;
}
/* Remove default focus outline */
.search-bar input.search-input:focus,
.search-bar button.search-btn:focus {
outline: none;
box-shadow: none;
}
/* Bookmark grid */
.bookmarks {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
max-width: 800px;
}
/* Individual bookmark tile */
.bookmark {
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(6px);
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
color: var(--text);
width: 100px;
height: 100px;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transform: translateY(0) scale(1);
}
.bookmark:hover {
transform: translateY(-4px) scale(1.1);
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.bookmark-icon {
font-size: 1.75rem;
margin-bottom: 0.25rem;
/* accentuate icons & add-button */
color: var(--accent);
}
.bookmark-title {
font-size: 0.8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.delete-btn {
position: absolute;
top: 5px;
right: 7px;
background: none;
border: none;
color: red;
font-size: 1rem;
cursor: pointer;
}
/* Add button style */
.add-bookmark {
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
border-radius: 50%;
font-size: 2rem;
background: rgba(255,255,255,0.05);
border: 1px dashed rgba(255,255,255,0.3);
backdrop-filter: blur(6px);
transition: transform 0.2s ease-in-out, background 0.3s, border-color 0.3s;
color: white;
transform: scale(1);
}
.add-bookmark:hover {
transform: scale(1.1);
}
/* Popup styling */
.popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(18,20,24,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 99;
}
.popup.hidden {
display: none;
}
.popup-inner {
background: var(--dark-purple);
padding: 2rem;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 1rem;
color: var(--text);
min-width: 300px;
}
.popup-inner input {
padding: 0.5rem;
background: var(--dark-blue);
border: none;
color: var(--text);
border-radius: 6px;
}
.popup-buttons {
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.popup-buttons button {
padding: 0.5rem 1rem;
background: var(--primary);
border: none;
border-radius: 6px;
color: white;
cursor: pointer;
}
.popup-buttons button:hover {
background: var(--accent);
}
/* Color Palette */
:root {
--bg: #121418;
--dark-blue: #0B1C2B;
--dark-purple: #1B1035;
--primary: #7B2EFF;
--accent: #00C6FF;
--text: #E0E0E0;
}
/* Icon grid styling */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
gap: 8px;
max-height: 200px;
overflow-y: auto;
margin-bottom: 8px;
}
.icon-item {
cursor: pointer;
padding: 4px;
border: 1px solid transparent;
border-radius: 4px;
text-align: center;
}
.icon-item:hover {
background: rgba(0, 0, 0, 0.1);
}
.icon-item.selected {
border-color: #0078d4;
background: rgba(0, 120, 212, 0.1);
}
+52
View File
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>New Tab</title>
<link rel="icon" href="../assets/images/Logos/Nebula-Logo.svg" type="image/png">
<link rel="stylesheet" href="home.css">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
rel="stylesheet">
</head>
<body>
<div class="home-container">
<div class="logo">
<img src="../assets/images/Logos/Nebula-Logo.svg" class="logo-img">
<div class="logo-text">Nebula Browser</div>
</div>
<div class="search-bar">
<input type="text" id="searchInput" class="search-input" placeholder="Search">
<button id="searchBtn" class="search-btn">
<span class="material-symbols-outlined">search</span>
</button>
</div>
<div class="bookmarks" id="bookmarkList">
<!-- Bookmarks dynamically inserted here -->
</div>
</div>
<!-- Popup for adding a bookmark -->
<div id="addPopup" class="popup hidden">
<div class="popup-inner">
<h2>Add Bookmark</h2>
<!-- ICON GRID SELECTOR -->
<label>Icon:</label>
<input type="text" id="iconFilter" placeholder="Search icons…" class="icon-filter">
<div id="iconGrid" class="icon-grid"></div>
<input type="hidden" id="selectedIcon">
<!-- TITLE & URL -->
<input type="text" id="titleInput" placeholder="Title">
<input type="url" id="urlInput" placeholder="https://...">
<div class="popup-buttons">
<button id="saveBookmarkBtn">Save</button>
<button id="cancelBtn">Cancel</button>
</div>
</div>
</div>
<!-- make this a module so we can import icons -->
<script type="module" src="home.js"></script>
</body>
</html>
+136
View File
@@ -0,0 +1,136 @@
import { icons } from './icons.js';
const BOOKMARKS_KEY = 'steamos_browser_bookmarks';
const bookmarkList = document.getElementById('bookmarkList');
const titleInput = document.getElementById('titleInput');
const urlInput = document.getElementById('urlInput');
const saveBookmarkBtn = document.getElementById('saveBookmarkBtn');
const cancelBtn = document.getElementById('cancelBtn');
const addPopup = document.getElementById('addPopup');
const searchBtn = document.getElementById('searchBtn');
const searchInput = document.getElementById('searchInput');
const iconFilter = document.getElementById('iconFilter');
const iconGrid = document.getElementById('iconGrid');
const selectedIconInput= document.getElementById('selectedIcon');
let selectedIcon = icons[0];
let bookmarks = JSON.parse(localStorage.getItem(BOOKMARKS_KEY)) || [];
function saveBookmarks() {
localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
}
function renderBookmarks() {
const list = JSON.parse(localStorage.getItem(BOOKMARKS_KEY) || '[]');
bookmarkList.innerHTML = '';
// Render each bookmark
list.forEach((b, index) => {
const box = document.createElement('div');
box.className = 'bookmark';
// prepend icon
const iconEl = document.createElement('span');
iconEl.className = 'material-symbols-outlined';
iconEl.textContent = b.icon || 'bookmark';
box.appendChild(iconEl);
const label = document.createElement('span');
label.className = 'bookmark-title';
label.textContent = b.title;
const close = document.createElement('button');
close.textContent = '×';
close.className = 'delete-btn';
close.onclick = (e) => {
e.stopPropagation();
bookmarks.splice(index, 1);
saveBookmarks();
renderBookmarks();
};
box.onclick = () => window.location.href = b.url;
box.appendChild(label);
box.appendChild(close);
bookmarkList.appendChild(box);
});
// Add "+" box
const addBox = document.createElement('div');
addBox.className = 'bookmark add-bookmark';
addBox.textContent = '+';
addBox.onclick = () => addPopup.classList.remove('hidden');
bookmarkList.appendChild(addBox);
}
// draw the icongrid, filtering by the search term
function renderIconGrid(filter = '') {
iconGrid.innerHTML = '';
icons
.filter(name => name.includes(filter))
.forEach(name => {
const span = document.createElement('span');
span.className = 'material-symbols-outlined icon-item';
span.textContent = name;
span.onclick = () => {
iconGrid.querySelectorAll('.icon-item')
.forEach(el => el.classList.remove('selected'));
span.classList.add('selected');
selectedIcon = name;
selectedIconInput.value = name;
};
iconGrid.appendChild(span);
});
const first = iconGrid.querySelector('.icon-item');
if (first) first.click();
}
// filter as the user types
iconFilter.addEventListener('input', () =>
renderIconGrid(iconFilter.value.trim().toLowerCase())
);
// initial render
renderIconGrid();
saveBookmarkBtn.onclick = () => {
const title = titleInput.value.trim();
const url = urlInput.value.trim();
const icon = selectedIcon;
if (!title || !url) return;
bookmarks.push({ title, url, icon });
saveBookmarks();
renderBookmarks();
titleInput.value = '';
urlInput.value = '';
addPopup.classList.add('hidden');
};
cancelBtn.onclick = () => {
addPopup.classList.add('hidden');
};
searchBtn.addEventListener('click', () => {
const input = searchInput.value.trim();
const hasProtocol = /^https?:\/\//i.test(input);
const looksLikeUrl = hasProtocol || /\./.test(input);
let target;
if (looksLikeUrl) {
target = hasProtocol ? input : `https://${input}`;
} else {
target = `https://www.google.com/search?q=${encodeURIComponent(input)}`;
}
window.location.href = target;
});
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') searchBtn.click();
});
// initial render from localStorage
renderBookmarks();
+143
View File
@@ -0,0 +1,143 @@
export const icons = [
'home',
'star',
'bookmark',
'favorite',
'public',
'search',
'settings',
'3d_rotation',
'ac_unit',
'access_alarm',
'access_alarms',
'access_time',
'accessibility',
'accessibility_new',
'accessible',
'accessible_forward',
'account_balance',
'account_balance_wallet',
'account_box',
'account_circle',
'adb',
'add',
'add_a_photo',
'add_alarm',
'add_alert',
'add_box',
'add_business',
'add_call',
'add_circle',
'add_circle_outline',
'add_comment',
'add_home',
'add_ic_call',
'add_link',
'add_location',
'add_photo_alternate',
'add_road',
'add_shopping_cart',
'add_task',
'add_to_drive',
'add_to_home_screen',
'add_to_photos',
'add_to_queue',
'adjust',
'admin_panel_settings',
'agriculture',
'airline_seat_flat',
'airline_seat_flat_angled',
'airline_seat_individual_suite',
'airline_seat_legroom_extra',
'airline_seat_legroom_normal',
'airline_seat_legroom_reduced',
'airline_seat_recline_extra',
'airline_seat_recline_normal',
'airplanemode_active',
'airplanemode_inactive',
'airplay',
'airport_shuttle',
'alarm',
'alarm_add',
'alarm_off',
'alarm_on',
'album',
'align_horizontal_center',
'align_horizontal_left',
'align_horizontal_right',
'align_vertical_bottom',
'align_vertical_center',
'align_vertical_top',
'all_inbox',
'all_inclusive',
'all_out',
'alt_route',
'analytics',
'anchor',
'android',
'animation',
'announcement',
'apartment',
'api',
'app_blocking',
'app_registration',
'app_settings_alt',
'approval',
'apps',
'archive',
'area_chart',
'arrow_back',
'arrow_back_ios',
'arrow_circle_down',
'arrow_circle_up',
'arrow_downward',
'arrow_drop_down',
'arrow_drop_down_circle',
'arrow_drop_up',
'arrow_forward',
'arrow_forward_ios',
'arrow_left',
'arrow_right',
'arrow_right_alt',
'arrow_upward',
'art_track',
'article',
'aspect_ratio',
'assessment',
'assignment',
'assignment_ind',
'assignment_late',
'assignment_return',
'assignment_returned',
'assignment_turned_in',
'assistant',
'assistant_photo',
'atm',
'attach_email',
'attach_file',
'attach_money',
'attachment',
'attractions',
'attribution',
'audiotrack',
'auto_awesome',
'auto_awesome_mosaic',
'auto_delete',
'auto_fix_high',
'auto_fix_normal',
'auto_fix_off',
'auto_graph',
'auto_stories',
'autorenew',
'av_timer',
'baby_changing_station',
'backpack',
'backspace',
'backup',
'badge',
'zoom_in',
'zoom_out',
'zoom_out_map'
];
//Icons from fonts.google.com/icons
+10
View File
@@ -0,0 +1,10 @@
[
"home",
"star",
"bookmark",
"favorite",
"public",
"search",
"settings"
// … add as many icon names as you like …
]
+80
View File
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Nebula Browser</title>
<link rel="stylesheet" href="style.css">
<style>
/* Removed custom draggable bar CSS to allow use of native title bar */
:root { --resize-border: 8px; }
body {
padding: var(--resize-border);
margin: 0;
height: calc(100vh - 2 * var(--resize-border));
display: flex;
flex-direction: column;
box-sizing: border-box;
}
::placeholder {
color: rgba(255, 255, 255, 0.5);
/* Adjust the color and transparency as needed */
}
</style>
</head>
<body>
<div id="tab-bar"></div>
<div id="nav">
<div class="nav-left">
<button onclick="goBack()"></button>
<button onclick="goForward()"></button>
<button id="reload-btn"></button>
</div>
<div class="nav-center">
<input id="url" type="text" placeholder="Type URL here" />
<button onclick="navigate()">Go</button>
</div>
<div class="nav-right">
<button title="Downloads"></button>
<div class="menu-wrapper">
<button id="menu-btn"></button>
<div id="menu-popup" class="hidden">
<button onclick="openSettings()">Settings</button>
<!-- You can add more options here -->
<div class="zoom-controls">
<button id="zoom-out-btn">-</button>
<span id="zoom-percent">100%</span>
<button id="zoom-in-btn">+</button>
</div>
</div>
</div>
</div>
</div>
<div id="webviews">
<webview id="webview" src="https://example.com"></webview>
</div>
<script src="script.js"></script>
<script>
const { ipcRenderer } = require('electron');
const webview = document.getElementById('webview');
webview.addEventListener('did-navigate', (e) => {
// save site URL
ipcRenderer.invoke('save-site-history', e.url);
// extract search query if present
const m = /[?&](?:q|query)=([^&]+)/.exec(e.url);
if (m && m[1]) {
const query = decodeURIComponent(m[1].replace(/\+/g, ' '));
ipcRenderer.invoke('save-search-history', query);
}
});
</script>
</body>
</html>
+475
View File
@@ -0,0 +1,475 @@
const ipcRenderer = window.electronAPI;
// 1) cache hot DOM references
const urlBox = document.getElementById('url');
const tabBarEl = document.getElementById('tab-bar');
const webviewsEl = document.getElementById('webviews');
const menuPopup = document.getElementById('menu-popup');
const contextMenu = document.getElementById('context-menu');
const menuItems = contextMenu ? contextMenu.querySelectorAll('li') : [];
// Select all text on focus and prevent mouseup from deselecting
urlBox.addEventListener('focus', () => urlBox.select());
urlBox.addEventListener('mouseup', e => e.preventDefault());
// Add Enter key navigation
urlBox.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
navigate();
}
});
let tabs = [];
let activeTabId = null;
const allowedInternalPages = ['settings', 'home'];
let bookmarks = [];
function createTab(inputUrl) {
inputUrl = inputUrl || 'browser://home';
console.log('[DEBUG] createTab() inputUrl =', inputUrl);
const id = crypto.randomUUID();
const resolvedUrl = resolveInternalUrl(inputUrl);
console.log('[DEBUG] createTab() resolvedUrl =', resolvedUrl);
const webview = document.createElement('webview');
webview.id = `tab-${id}`;
webview.src = resolvedUrl;
webview.setAttribute('allowpopups', '');
webview.setAttribute('partition', 'persist:default');
webview.classList.add('active');
webview.addEventListener('did-fail-load', handleLoadFail(id));
webview.addEventListener('page-title-updated', e => updateTabMetadata(id, 'title', e.title));
webview.addEventListener('page-favicon-updated', e => {
if (e.favicons.length > 0) updateTabMetadata(id, 'favicon', e.favicons[0]);
});
webview.addEventListener('did-navigate', e => handleNavigation(id, e.url)); // was using inputUrl
webview.addEventListener('did-navigate-in-page', e => handleNavigation(id, e.url)); // was using inputUrl
// catch any target="_blank" or window.open() calls and open them as new tabs
webview.addEventListener('new-window', e => {
e.preventDefault();
createTab(e.url);
});
webviewsEl.appendChild(webview);
tabs.push({
id,
url: inputUrl, // ← save the original input like "browser://home"
title: 'New Tab',
favicon: null,
history: [inputUrl],
historyIndex: 0
});
setActiveTab(id);
renderTabs();
}
function resolveInternalUrl(url) {
if (url.startsWith('browser://')) {
const page = url.replace('browser://', '');
if (allowedInternalPages.includes(page)) return `${page}.html`;
else return '404.html';
}
return url.startsWith('http') ? url : `https://${url}`;
}
function handleLoadFail(tabId) {
return (event) => {
if (!event.validatedURL.includes('browser://') && event.errorCode !== -3) {
const webview = document.getElementById(`tab-${tabId}`);
webview.src = `404.html?url=${encodeURIComponent(tabs.find(t => t.id === tabId).url)}`;
}
};
}
function updateTabMetadata(id, key, value) {
const tab = tabs.find(t => t.id === id);
if (tab) {
tab[key] = value;
renderTabs();
}
}
function navigate() {
const input = urlBox.value.trim();
const tab = tabs.find(t => t.id === activeTabId);
const webview = document.getElementById(`tab-${activeTabId}`);
if (!tab || !webview) return;
// decide if this is a search query or a URL/internal page
const hasProtocol = /^https?:\/\//i.test(input);
const isInternal = input.startsWith('browser://');
const isLikelyUrl = hasProtocol || input.includes('.');
let resolved;
if (!isInternal && !isLikelyUrl) {
resolved = `https://www.google.com/search?q=${encodeURIComponent(input)}`;
} else {
resolved = resolveInternalUrl(input);
}
// Push to history using the original input
tab.history = tab.history.slice(0, tab.historyIndex + 1);
tab.history.push(input);
tab.historyIndex++;
tab.url = input;
webview.src = resolved;
renderTabs();
updateNavButtons();
}
function handleNavigation(tabId, newUrl) {
const tab = tabs.find(t => t.id === tabId);
if (!tab) return;
// --- record every real navigation into history ---
if (tab.history[tab.historyIndex] !== newUrl) {
tab.history = tab.history.slice(0, tab.historyIndex + 1);
tab.history.push(newUrl);
tab.historyIndex++;
}
// translate local files back to our browser:// scheme
const isHome = newUrl.endsWith('home.html');
const isSettings = newUrl.endsWith('settings.html');
const displayUrl = isHome
? 'browser://home'
: isSettings
? 'browser://settings'
: newUrl;
tab.url = displayUrl;
if (tabId === activeTabId) {
urlBox.value = displayUrl === 'browser://home' ? '' : displayUrl;
}
renderTabs();
updateNavButtons();
}
function setActiveTab(id) {
tabs.forEach(t => {
const w = document.getElementById(`tab-${t.id}`);
if (w) w.classList.remove('active');
});
const activeWebview = document.getElementById(`tab-${id}`);
if (activeWebview) activeWebview.classList.add('active');
activeTabId = id;
const tab = tabs.find(t => t.id === id);
if (tab) {
// If the tab URL represents the home page, keep the URL bar blank.
urlBox.value = tab.url === 'browser://home' ? '' : tab.url;
renderTabs();
updateNavButtons();
updateZoomUI(); // ← update zoom display for new active tab
}
}
function closeTab(id) {
const w = document.getElementById(`tab-${id}`);
if (w) w.remove();
tabs = tabs.filter(t => t.id !== id);
if (id === activeTabId) {
if (tabs.length > 0) setActiveTab(tabs[0].id);
}
renderTabs();
updateNavButtons();
}
// 2) streamline renderTabs with a fragment
function renderTabs() {
const frag = document.createDocumentFragment();
tabs.forEach(tab => {
const el = document.createElement('div');
el.className = 'tab' + (tab.id === activeTabId ? ' active' : '');
if (tab.favicon) {
const icon = document.createElement('img');
icon.src = tab.favicon;
icon.style.width = '16px';
icon.style.height = '16px';
icon.style.marginRight = '6px';
el.appendChild(icon);
}
el.appendChild(document.createTextNode(tab.title || new URL(tab.url).hostname));
const closeBtn = document.createElement('button');
closeBtn.textContent = '×';
closeBtn.onclick = (e) => {
e.stopPropagation();
closeTab(tab.id);
};
// 2a) make tab draggable
el.draggable = true;
el.addEventListener('dragstart', e => {
e.dataTransfer.setData('tabId', tab.id);
});
// 2b) on dragend outside window, open in new window and close here
el.addEventListener('dragend', e => {
if (
e.clientX < 0 || e.clientX > window.innerWidth ||
e.clientY < 0 || e.clientY > window.innerHeight
) {
ipcRenderer.invoke('open-tab-in-new-window', tab.url);
closeTab(tab.id);
}
});
el.onclick = () => setActiveTab(tab.id);
el.appendChild(closeBtn);
frag.appendChild(el);
});
// add the “+” at the end
const plus = document.createElement('div');
plus.className = 'tab'; plus.textContent = '+'; plus.onclick = () => createTab();
frag.appendChild(plus);
tabBarEl.innerHTML = ''; // clear once
tabBarEl.appendChild(frag); // append in one shot
}
// 1) handle URL sent by main for a detached window
ipcRenderer.on('open-url', (event, url) => {
tabs = [];
activeTabId = null;
webviewsEl.innerHTML = '';
tabBarEl.innerHTML = '';
createTab(url);
});
function goBack() {
const webview = document.getElementById(`tab-${activeTabId}`);
if (webview && webview.canGoBack()) {
webview.goBack();
}
}
function goForward() {
const webview = document.getElementById(`tab-${activeTabId}`);
if (webview && webview.canGoForward()) {
webview.goForward();
}
}
function updateNavButtons() {
const webview = document.getElementById(`tab-${activeTabId}`);
const backBtn = document.querySelector('.nav-left button:nth-child(1)');
const forwardBtn = document.querySelector('.nav-left button:nth-child(2)');
backBtn.disabled = !webview || !webview.canGoBack();
forwardBtn.disabled = !webview || !webview.canGoForward();
}
function reload() {
const webview = document.getElementById(`tab-${activeTabId}`);
if (webview) {
webview.reload();
updateNavButtons(); // keep back/forward buttons in sync after a reload
}
}
function openSettings() {
urlBox.value = 'browser://settings';
navigate();
}
// Toggle menu dropdown
const menuBtn = document.getElementById('menu-btn');
menuBtn.addEventListener('click', () => {
menuPopup.classList.toggle('hidden');
if (!menuPopup.classList.contains('hidden')) {
updateZoomUI(); // ← refresh zoom % whenever menu opens
}
});
window.addEventListener('DOMContentLoaded', () => {
createTab();
// only now bind the reload button (guaranteed to exist)
const reloadBtn = document.getElementById('reload-btn');
reloadBtn.addEventListener('click', reload);
// bind zoom buttons (single binding)
const zoomInBtn = document.getElementById('zoom-in-btn');
const zoomOutBtn = document.getElementById('zoom-out-btn');
zoomInBtn.addEventListener('click', zoomIn);
zoomOutBtn.addEventListener('click', zoomOut);
// wire up back/forward buttons
const backBtn = document.querySelector('.nav-left button:nth-child(1)');
const forwardBtn = document.querySelector('.nav-left button:nth-child(2)');
backBtn.addEventListener('click', goBack);
forwardBtn.addEventListener('click', goForward);
// window control bindings
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';
}
}
// update initial zoom display
ipcRenderer.invoke('get-zoom-factor').then(z => {
document.getElementById('zoom-percent').textContent = `${Math.round(z * 100)}%`;
});
// menurelated code (moved here so #context-menu exists)
const items = menu ? menu.querySelectorAll('li') : [];
function showContextMenu(x, y) {
if (!menu) return;
menu.style.top = `${y}px`;
menu.style.left = `${x}px`;
menu.classList.add('visible');
}
document.addEventListener('contextmenu', e => {
if (e.target.tagName === 'WEBVIEW' ||
e.composedPath().some(el => el.id === 'webviews')) {
e.preventDefault();
showContextMenu(e.clientX, e.clientY);
}
});
document.addEventListener('click', () => {
if (menu) menu.classList.remove('visible');
});
items.forEach(item => {
item.addEventListener('click', async () => {
const action = item.dataset.action;
const win = remote.getCurrentWindow();
switch (action) {
case 'save-page': {
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' });
if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete');
break;
}
case 'select-all':
document.execCommand('selectAll');
break;
case 'screenshot': {
const image = await win.webContents.capturePage();
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' });
if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG());
break;
}
case 'view-source': {
const html = document.documentElement.outerHTML;
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' });
if (!canceled && filePath) fs.writeFileSync(filePath, html);
break;
}
case 'inspect-accessibility':
win.webContents.inspectAccessibilityNode(e.clientX, e.clientY);
break;
case 'inspect-element':
win.webContents.inspectElement(e.clientX, e.clientY);
break;
}
menu.classList.remove('visible');
});
});
// ipcRenderer.invoke('load-bookmarks').then(bs => {
// bookmarks = bs;
// console.log('[DEBUG] Loaded bookmarks:', bookmarks);
// });
});
// zoom helpers
function updateZoomUI() {
const zp = document.getElementById('zoom-percent');
if (zp) {
ipcRenderer.invoke('get-zoom-factor').then(zf => {
// just show "NN%", not "Zoom: NN%"
zp.textContent = `${Math.round(zf * 100)}%`;
});
}
}
function zoomIn() { ipcRenderer.invoke('zoom-in').then(updateZoomUI); }
function zoomOut() { ipcRenderer.invoke('zoom-out').then(updateZoomUI); }
const fs = require('fs');
const { remote } = require('electron');
// 4) unify context-menu wiring
function showContextMenu(x,y) {
if (!contextMenu) return;
contextMenu.style.top = `${y}px`;
contextMenu.style.left = `${x}px`;
contextMenu.classList.add('visible');
}
document.addEventListener('contextmenu', e => {
if (e.target.tagName==='WEBVIEW' || e.composedPath().some(el=>el.id==='webviews')) {
e.preventDefault();
showContextMenu(e.clientX, e.clientY);
}
});
document.addEventListener('click', ()=> contextMenu && contextMenu.classList.remove('visible'));
menuItems.forEach(item => item.addEventListener('click', async evt => {
const action = item.dataset.action;
const win = remote.getCurrentWindow();
switch (action) {
case 'save-page': {
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'page.html' });
if (!canceled && filePath) win.webContents.savePage(filePath, 'HTMLComplete');
break;
}
case 'select-all':
document.execCommand('selectAll');
break;
case 'screenshot': {
const image = await win.webContents.capturePage();
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'screenshot.png' });
if (!canceled && filePath) fs.writeFileSync(filePath, image.toPNG());
break;
}
case 'view-source': {
const html = document.documentElement.outerHTML;
const { canceled, filePath } = await remote.dialog.showSaveDialog(win, { defaultPath: 'source.html' });
if (!canceled && filePath) fs.writeFileSync(filePath, html);
break;
}
case 'inspect-accessibility':
win.webContents.inspectAccessibilityNode(e.clientX, e.clientY);
break;
case 'inspect-element':
win.webContents.inspectElement(e.clientX, e.clientY);
break;
}
contextMenu.classList.remove('visible');
}));
+142
View File
@@ -0,0 +1,142 @@
:root {
--bg: #121418;
--dark-blue: #0B1C2B;
--dark-purple: #1B1035;
--primary: #7B2EFF;
--accent: #00C6FF;
--text: #E0E0E0;
}
/* Load InterVariable */
@font-face {
font-family: 'InterVariable';
src: url('../assets/images/fonts/InterVariable.ttf') format('truetype');
font-weight: 100 900;
font-display: swap;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: 'InterVariable', sans-serif;
margin: 0;
padding: 2rem;
display: flex;
justify-content: center;
align-items: flex-start;
height: 100vh;
}
.container {
background-color: var(--dark-purple);
padding: 2rem;
border-radius: 16px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
max-width: 500px;
width: 100%;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: var(--primary);
}
.setting-group {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
label {
font-weight: bold;
margin-bottom: 0.5rem;
}
input {
padding: 0.6rem;
font-size: 1rem;
border: none;
border-radius: 8px;
margin-bottom: 0.75rem;
background-color: var(--dark-blue);
color: var(--text);
}
button {
padding: 0.6rem;
font-size: 1rem;
background-color: var(--primary);
color: var(--text);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease-in-out;
}
button:hover {
background-color: var(--accent);
}
.note {
font-size: 0.8rem;
color: #aaa;
margin-top: 1rem;
}
.status {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.5rem;
background-color: rgba(18,20,24,0.8);
color: white;
padding: 0.8rem 1.2rem;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
font-size: 1rem;
z-index: 1000;
}
.status.hidden {
display: none;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.setting-group input,
.setting-group button {
width: 100%;
box-sizing: border-box;
}
/* small-screen adjustments */
@media (max-width: 480px) {
.container {
padding: 1rem;
border-radius: 0;
box-shadow: none;
}
h1 {
font-size: 1.25rem;
}
}
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Settings</title>
<link rel="stylesheet" href="settings.css" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙️</text></svg>">
<style>
body { font-family: sans-serif; padding: 20px; }
section { margin-bottom: 30px; }
h2 { border-bottom: 1px solid #ccc; padding-bottom: 5px; }
ul { list-style: none; padding-left: 0; }
li { padding: 5px 0; border-bottom: 1px solid #eee; }
</style>
</head>
<body>
<div class="container">
<h1>⚙️ Browser Settings</h1>
<div class="setting-group">
<label for="clear-data-btn">Clear All Cookies &amp; Data</label>
<button id="clear-data-btn">Clear Data</button>
</div>
<p class="note">Settings are stored locally on this device.</p>
</div>
<!-- status overlay moved outside of .container -->
<div id="status" class="status hidden">
<div class="spinner"></div>
<span id="status-text"></span>
</div>
<script src="settings.js"></script>
<script>
const { ipcRenderer } = require('electron');
async function loadHistories() {
try {
const searchHistory = await ipcRenderer.invoke('load-search-history');
const siteHistory = await ipcRenderer.invoke('load-site-history');
const searchList = document.getElementById('search-history-list');
const siteList = document.getElementById('site-history-list');
// Clear existing content
searchList.innerHTML = '';
siteList.innerHTML = '';
// Populate search history
searchHistory.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
searchList.appendChild(li);
});
// Populate site history
siteHistory.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
siteList.appendChild(li);
});
} catch(err) {
console.error('Error loading histories:', err);
}
}
// Load histories on page load.
window.addEventListener('DOMContentLoaded', loadHistories);
</script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
const { ipcRenderer } = require('electron');
const clearBtn = document.getElementById('clear-data-btn');
const statusDiv = document.getElementById('status');
const statusText = document.getElementById('status-text');
function showStatus(message) {
statusText.textContent = message;
statusDiv.classList.remove('hidden'); // Ensure the hidden class is removed
setTimeout(() => {
statusDiv.classList.add('hidden'); // Add the hidden class back after 2 seconds
}, 2000);
}
clearBtn.onclick = async () => {
statusDiv.classList.remove('hidden'); // Show spinner immediately
statusText.textContent = 'Clearing all browser data...'; // Update text while clearing
try {
// Invoke the main process to clear cookies, local storage, and cache
const ok = await ipcRenderer.invoke('clear-browser-data');
showStatus(ok
? 'All browser data and bookmarks cleared!'
: 'Failed to clear browser data.');
} catch (error) {
console.error('Error clearing browser data:', error);
showStatus('An error occurred while clearing data.');
}
};
+254
View File
@@ -0,0 +1,254 @@
html, body {
height: 100%;
margin: 0;
padding: 0;
background: #111;
color: white;
font-family: 'Segoe UI', sans-serif;
}
#tab-bar {
display: flex;
padding-left: 80px; /* leave room for macOS traffic lights */
overflow-x: auto; /* allow scrolling when many tabs */
/* custom scrollbar styling */
scrollbar-color: #444 #2a2a3c; /* thumb and track for Firefox */
scrollbar-width: thin; /* slimmer track */
}
#tab-bar > * {
flex: 1 1 0;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* NAVBAR LAYOUT */
#nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
background: #1e1e2e;
gap: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.nav-left,
.nav-center,
.nav-right {
display: flex;
align-items: center;
gap: 8px;
}
.nav-center {
flex: 1;
background: #2a2a3c;
padding: 4px 6px;
border-radius: 6px;
}
#favicon {
width: 16px;
height: 16px;
margin-right: 4px;
}
#url {
flex: 1;
background: transparent;
border: none;
color: white;
font-size: 14px;
outline: none;
}
#url::placeholder {
color: rgba(255, 255, 255, 0.5);
}
#nav button {
background: #333;
color: white;
border: none;
padding: 6px 10px;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
#nav button:hover {
background: #555;
}
/* MENU DROPDOWN */
.menu-wrapper {
position: relative;
}
#menu-popup {
position: absolute;
top: 30px;
right: 0;
background: #2a2a3c;
border-radius: 4px;
padding: 4px;
display: flex;
flex-direction: column;
min-width: 200px; /* wider dropdown */
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
z-index: 100;
}
#menu-popup button {
background: none;
border: none;
color: white;
text-align: left;
padding: 6px 10px;
border-radius: 4px;
}
#menu-popup button:hover {
background: #444;
}
.hidden {
display: none;
}
#menu-popup.hidden {
display: none;
}
/* WEBVIEWS */
#webviews {
flex: 1;
display: flex;
width: 100%;
position: relative;
}
#webviews webview {
flex: 1;
display: none;
border: none;
}
#webviews webview.active {
display: flex;
}
/* TABS */
.tab {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
margin: 4px 4px 0 0;
background: #222;
border-radius: 8px 8px 0 0;
cursor: pointer;
transition: background 0.2s, flex 0.2s;
min-width: 80px; /* prevent tabs from getting too small */
}
.tab:hover {
background: #333;
}
.tab.active {
background: #444;
font-weight: bold;
flex: 3 1 0; /* increased grow factor for larger active tab */
min-width: 120px; /* larger min width for the active tab */
}
.tab img {
width: 16px;
height: 16px;
margin-right: 6px;
border-radius: 2px;
}
.tab button {
margin-left: 8px;
background: none;
border: none;
color: #f55;
font-weight: bold;
cursor: pointer;
}
/* ZOOM CONTROLS */
.zoom-controls {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
}
.zoom-controls .zoom-label {
flex: 1;
font-size: 14px;
}
.zoom-controls button {
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
}
.zoom-controls button:hover {
background: #444;
}
#zoom-percent {
min-width: 36px;
text-align: center;
font-size: 14px;
}
/* window controls (Windows only) */
#window-controls {
position: absolute;
top: 0;
right: 0;
display: flex;
gap: 2px;
padding: 4px;
z-index: 200;
}
#window-controls button {
width: 46px;
height: 28px;
background: transparent;
border: none;
color: white;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
#window-controls button:hover {
background: rgba(255,255,255,0.1);
}
#window-controls #close-btn:hover {
background: #e81123;
}
#tab-bar::-webkit-scrollbar {
height: 8px; /* horizontal scrollbar height */
}
#tab-bar::-webkit-scrollbar-track {
background: #2a2a3c;
border-radius: 4px;
}
#tab-bar::-webkit-scrollbar-thumb {
background: #444;
border-radius: 4px;
}
#tab-bar::-webkit-scrollbar-thumb:hover {
background: #555;
}
+4
View File
@@ -0,0 +1,4 @@
[
"file:///X:/Projects/Code/NebulaBrowser/renderer/index.html",
"file:///X:/Projects/Code/SteamOS_Browser/renderer/index.html"
]
-55
View File
@@ -1,55 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden; /* Prevent body scroll, BrowserView will handle its own scroll */
background-color: #f0f0f0;
}
.navbar {
display: flex;
align-items: center;
padding: 10px;
background-color: #333;
color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
height: 60px; /* Adjust based on navBarHeight in main.js */
flex-shrink: 0; /* Prevent navbar from shrinking */
}
.navbar button {
background-color: #555;
color: white;
border: none;
padding: 8px 15px;
margin: 0 5px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s ease;
}
.navbar button:hover {
background-color: #777;
}
.navbar button:active {
background-color: #222;
}
.navbar #address-bar {
flex-grow: 1;
padding: 8px 10px;
border: 1px solid #777;
border-radius: 4px;
font-size: 16px;
background-color: #444;
color: white;
outline: none;
}
.navbar #address-bar:focus {
border-color: #007bff;
}