Neb added
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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>You’ve 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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 icon‐grid, 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();
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
"home",
|
||||
"star",
|
||||
"bookmark",
|
||||
"favorite",
|
||||
"public",
|
||||
"search",
|
||||
"settings"
|
||||
// … add as many icon names as you like …
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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)}%`;
|
||||
});
|
||||
|
||||
// menu‐related 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');
|
||||
}));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 & 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>
|
||||
@@ -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.');
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user