Add Nebula Browser app, UI and assets
Add initial Nebula Browser project skeleton: CMakeLists to configure and link CEF (including post-build steps to copy runtime and UI files), a Windows CEF-based entry (app/main.cpp) that initializes CEF and loads the bundled UI, and a full ui/ and assets/ tree (HTML, CSS, JS, fonts, icons, and branding images). Update .gitignore to ignore build/out, thirdparty/cef, IDE and common OS artifacts.
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xodm="http://www.corel.com/coreldraw/odm/2003" viewBox="0 0 396 537">
|
||||
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: url(#linear-gradient);
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="33174.52" y1="-10318.51" x2="32748.82" y2="-30894.15" gradientTransform="translate(-738.46 -250.12) scale(.03 -.03)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#26b8f4"/>
|
||||
<stop offset="1" stop-color="#1b48ef"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Camada_x5F_1">
|
||||
<polygon class="st0" points="18.24 7.68 121.67 44.11 122.02 406.95 266.22 322.9 195.86 289.99 150.71 177.85 379.62 258.02 379.43 375.52 121.65 524 18.58 466.65 18.24 7.68"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 976 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.a{fill:#d53;}.b{fill:#fff;}.c{fill:#ddd;}.d{fill:#fc0;}.e{fill:#6b5;}.f{fill:#4a4;}.g{fill:#148;}</style></defs><title>duckduckgo</title><path class="a" d="M122.88,61.44a61.44,61.44,0,1,0-61.44,61.44,61.44,61.44,0,0,0,61.44-61.44Z"/><path class="b" d="M114.37,61.44a52.92,52.92,0,1,0-15.5,37.43,52.76,52.76,0,0,0,15.5-37.43Zm-13.12-39.8A56.29,56.29,0,1,1,61.44,5.15a56.12,56.12,0,0,1,39.81,16.49Z"/><path class="c" d="M43.24,30.15C26.17,34.13,32.43,58,32.43,58l10.81,52.9,4,1.71-4-82.49Zm-4-10.24H34.7L41,22.19s-6.26,0-6.26,4C48.36,25.6,54.61,29,54.61,29l-15.36-9.1Zm0,0Z"/><path class="b" d="M75.66,115.48S62,93.87,62,79.64c0-26.73,17.63-4,17.63-25S62,28.44,62,28.44c-8.53-10.8-25-8.53-25-8.53l4,2.28s-4,1.13-5.12,2.27,10.81-1.7,15.93,2.85C30.72,29,34.13,46.08,34.13,46.08l11.95,68.27,29.58,1.13Zm0,0Z"/><path class="d" d="M75.66,60.87l21.62-5.69C116.62,58,80.78,68.84,78.51,68.27c-17.07-2.85-12,11.37,8.53,6.82s5.12,11.38-13.65,5.12c-26.74-7.39-12.52-20.48,2.27-19.34Z"/><path class="e" d="M70,105.81l1.14-1.7c12.52,4.55,13.09,6.25,12.52-5.12s0-11.38-13.09-1.71c0-2.84-7.39-1.71-8.53,0-11.95-5.12-13.09-6.83-12.52,1.14,1.14,16.5.57,13.65,11.95,8l8.53-.57Zm0,0Z"/><path class="f" d="M60.87,99.56v6.82c.57,1.14,9.67,1.14,9.67-1.14s-4.55,1.71-7.39.57S62,98.42,62,98.42l-1.14,1.14Zm0,0Z"/><path class="g" d="M48.36,43.24c-2.85-3.42-10.24-.57-8.54,4,.57-2.28,4.55-5.69,8.54-4Zm18.2,0c.57-3.42,6.26-4,8-.57a8,8,0,0,0-8,.57Zm-18.77,9.1a1.14,1.14,0,1,1,0,.57v-.57Zm-4.55,2.27a4,4,0,1,0,0-.57v.57Zm29.58-4a1.14,1.14,0,1,1,0,.57v-.57ZM69.4,52.91a3.42,3.42,0,1,0,0-.57v.57Zm0,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-3 0 262 262" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"/><path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"/><path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"/><path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 890 KiB |
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1526 1526">
|
||||
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-dasharray: 514.68 205.87;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 150px;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #1f0;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="412.1" y1="-275.22" x2="1113.92" y2="426.7" gradientTransform="translate(0 687.66)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#7b2eff"/>
|
||||
<stop offset="1" stop-color="#0086ff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_2" class="st2">
|
||||
<rect class="st1" x="-1545.34" y="-222.57" width="3604.86" height="2130.85"/>
|
||||
</g>
|
||||
<g id="Layer_11" data-name="Layer_1">
|
||||
<circle class="st0" cx="763" cy="763" r="688"/>
|
||||
<circle class="st3" cx="763.01" cy="763.4" r="496.3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 2832.51 1767.86">
|
||||
<!-- Generator: Adobe Illustrator 29.6.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 9) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #792fff;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #1f0;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
stroke-dasharray: 514.68 205.87;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 150px;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #0384ff;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="1599.65" y1="1234.69" x2="2301.48" y2="532.77" gradientTransform="translate(0 1767.66) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#7b2eff"/>
|
||||
<stop offset="1" stop-color="#0086ff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_2" class="st4">
|
||||
<rect class="st1" x="-357.79" y="-102.04" width="3604.86" height="2130.85"/>
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<circle class="st2" cx="1950.55" cy="883.53" r="688"/>
|
||||
<circle class="st5" cx="1950.56" cy="883.93" r="496.3"/>
|
||||
<rect class="st0" x="78.18" y="915.01" width="944.83" height="96.74" rx="48.37" ry="48.37"/>
|
||||
<rect class="st3" x="328.64" y="339.26" width="689.08" height="96.74" rx="48.37" ry="48.37"/>
|
||||
<rect class="st3" x="328.64" y="1466.3" width="689.08" height="96.74" rx="48.37" ry="48.37"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,81 @@
|
||||
/* Load InterVariable */
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url('../assets/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,606 @@
|
||||
/* Load InterVariable */
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url('../assets/fonts/InterVariable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* CSS Custom Properties for Theming */
|
||||
:root {
|
||||
--bg: #121418;
|
||||
--dark-blue: #0B1C2B;
|
||||
--dark-purple: #1B1035;
|
||||
--primary: #7B2EFF;
|
||||
--accent: #00C6FF;
|
||||
--text: #E0E0E0;
|
||||
--home-greeting-y: 12vh; /* fixed vertical baseline */
|
||||
--home-search-y: 22vh; /* user adjustable */
|
||||
--home-bookmarks-y: 40vh; /* user adjustable */
|
||||
}
|
||||
|
||||
/* Base reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
/* Use CSS custom properties for theming */
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 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: flex-start;
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
padding: 4rem 2rem 2rem;
|
||||
}
|
||||
|
||||
.edit-btn { position: fixed; top: 16px; right: 16px; z-index: 5; background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); border:1px solid color-mix(in srgb, var(--text) 20%, transparent); border-radius:8px; padding:6px 10px; cursor:pointer; backdrop-filter: blur(6px); }
|
||||
.edit-btn[aria-pressed="true"] { background: color-mix(in srgb, var(--text) 22%, transparent); }
|
||||
.edit-mode .edit-btn { display:none; }
|
||||
.edit-mode .greeting-title, .edit-mode .search-container, .edit-mode .top-sites-card, .edit-mode .glance { outline: 2px dashed color-mix(in srgb, var(--text) 35%, transparent); outline-offset: 4px; cursor: grab; }
|
||||
.edit-mode .glance.dragging { cursor: grabbing; }
|
||||
|
||||
/* Edit toolbar */
|
||||
.edit-toolbar { position: fixed; top: 16px; right: 16px; display:none; gap:10px; z-index:6; backdrop-filter: blur(8px); background: color-mix(in srgb, var(--bg) 50%, transparent); border:1px solid color-mix(in srgb, var(--text) 15%, transparent); padding:8px 10px; border-radius:12px; box-shadow: 0 12px 30px -14px color-mix(in srgb, var(--bg) 70%, transparent); }
|
||||
.edit-mode .edit-toolbar { display:flex; }
|
||||
.edit-toolbar[hidden] { display: none !important; }
|
||||
|
||||
/* Corner helpers for edit controls */
|
||||
.edit-btn.pos-br, .edit-toolbar.pos-br { right:16px; bottom:16px; left:auto; top:auto; }
|
||||
.edit-btn.pos-bl, .edit-toolbar.pos-bl { left:16px; bottom:16px; right:auto; top:auto; }
|
||||
.edit-btn.pos-tr, .edit-toolbar.pos-tr { right:16px; top:16px; left:auto; bottom:auto; }
|
||||
.edit-btn.pos-tl, .edit-toolbar.pos-tl { left:16px; top:16px; right:auto; bottom:auto; }
|
||||
.edit-toolbar .btn { min-width:90px; padding:8px 12px; border-radius:8px; border:1px solid transparent; color: var(--text); cursor:pointer; }
|
||||
.edit-toolbar .btn.primary { background: linear-gradient(135deg, var(--accent), var(--primary)); }
|
||||
.edit-toolbar .btn.secondary { background: color-mix(in srgb, var(--text) 14%, transparent); border-color: color-mix(in srgb, var(--text) 20%, transparent); }
|
||||
|
||||
/* Greeting hero title */
|
||||
.greeting-title {
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
color: var(--text);
|
||||
text-shadow: 0 4px 22px color-mix(in srgb, var(--bg) 60%, transparent);
|
||||
margin-bottom: 1.25rem;
|
||||
position: relative;
|
||||
top: var(--home-greeting-y);
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Utility: fully hide elements when user toggles them off */
|
||||
.is-hidden { display: none !important; }
|
||||
|
||||
/* Search bar container */
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 1.5rem;
|
||||
width: 680px;
|
||||
max-width: min(92vw, 900px);
|
||||
position: relative;
|
||||
z-index: 300; /* ensure dropdown overlays bookmarks/top-sites stacking contexts */
|
||||
top: var(--home-search-y);
|
||||
/* Unified glassy pill */
|
||||
background: color-mix(in srgb, var(--text) 12%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text) 20%, transparent);
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 18px 50px -22px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 12%, transparent);
|
||||
backdrop-filter: blur(10px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
||||
padding: 6px 8px;
|
||||
transition: box-shadow 180ms ease, border-color 180ms ease, background 180ms ease, transform 120ms ease;
|
||||
}
|
||||
|
||||
.search-container:hover {
|
||||
background: color-mix(in srgb, var(--text) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--text) 28%, transparent);
|
||||
}
|
||||
|
||||
.search-container:focus-within {
|
||||
box-shadow: 0 22px 60px -24px color-mix(in srgb, var(--bg) 90%, transparent), 0 0 0 2px color-mix(in srgb, var(--primary) 45%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 16%, transparent);
|
||||
border-color: color-mix(in srgb, var(--primary) 55%, transparent);
|
||||
}
|
||||
|
||||
/* Search bar */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex: 1; /* Take remaining space inside the pill */
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-radius: 9999px;
|
||||
padding: 0 6px 0 2px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.search-bar input.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0 10px 0 8px;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
color: var(--text);
|
||||
caret-color: var(--accent);
|
||||
}
|
||||
|
||||
.search-bar input.search-input::placeholder {
|
||||
color: color-mix(in srgb, var(--text) 55%, transparent);
|
||||
}
|
||||
|
||||
.search-bar button.search-btn {
|
||||
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
background: color-mix(in srgb, var(--bg) 45%, transparent);
|
||||
color: var(--text);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
transition: transform 120ms ease, background 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.search-bar button.search-btn:hover { transform: scale(1.02); background: color-mix(in srgb, var(--bg) 55%, transparent); border-color: color-mix(in srgb, var(--text) 24%, transparent); }
|
||||
.search-bar button.search-btn:active { transform: scale(0.98); }
|
||||
|
||||
.search-bar button.search-btn .material-symbols-outlined {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Search engine trigger unified look */
|
||||
.search-engine-selector { position: relative; display: flex; align-items: center; }
|
||||
.search-engine-btn {
|
||||
background: color-mix(in srgb, var(--bg) 45%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
|
||||
border-radius: 9999px;
|
||||
padding: 8px 10px 8px 12px;
|
||||
cursor: pointer;
|
||||
height: 44px;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
transition: background 160ms ease, border-color 160ms ease, transform 120ms ease;
|
||||
}
|
||||
.search-engine-btn:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); border-color: color-mix(in srgb, var(--text) 24%, transparent); }
|
||||
.search-engine-btn:active { transform: scale(0.98); }
|
||||
|
||||
.search-engine-btn img { width: 22px; height: 22px; filter: none; }
|
||||
|
||||
/* Subtle divider after the engine button */
|
||||
.search-engine-selector::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 1px;
|
||||
background: linear-gradient(to bottom, color-mix(in srgb, var(--text) 6%, transparent), color-mix(in srgb, var(--text) 24%, transparent), color-mix(in srgb, var(--text) 6%, transparent));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.search-container { width: 94vw; padding: 6px; }
|
||||
.search-bar { height: 42px; }
|
||||
.search-engine-btn { height: 42px; width: 46px; }
|
||||
.search-bar button.search-btn { width: 38px; height: 38px; }
|
||||
}
|
||||
|
||||
/* Remove default focus outline */
|
||||
.search-bar input.search-input:focus,
|
||||
.search-bar button.search-btn:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* (legacy Search Engine Selector block removed; unified styles are defined above) */
|
||||
|
||||
.search-engine-dropdown {
|
||||
position: absolute;
|
||||
top: 110%;
|
||||
left: 0;
|
||||
background: color-mix(in srgb, var(--bg) 94%, #000 6%);
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
|
||||
box-shadow: 0 18px 50px -22px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent);
|
||||
z-index: 100;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
/* Animated open/close */
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
transform-origin: top left;
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
max-height: 320px; /* enough for options; adjust if you add more */
|
||||
transition: opacity 160ms ease, transform 160ms ease, max-height 200ms ease;
|
||||
}
|
||||
|
||||
.search-engine-dropdown.hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px) scale(0.98);
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.search-engine-option {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-engine-option:hover { background-color: rgba(255,255,255,0.08); }
|
||||
|
||||
.search-engine-option img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Respect reduced motion preferences */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.search-engine-dropdown {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bookmark grid */
|
||||
.bookmarks {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* Top Sites card wrapper */
|
||||
.top-sites-card {
|
||||
width: min(900px, 96vw);
|
||||
margin-top: 1.25rem;
|
||||
padding: 1rem 1rem 1.25rem;
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, var(--text) 6%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
|
||||
box-shadow: 0 18px 50px -20px color-mix(in srgb, var(--bg) 60%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
position: relative;
|
||||
top: var(--home-bookmarks-y);
|
||||
}
|
||||
.top-sites-header {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
margin-bottom: 0.75rem; padding: 0 0.25rem;
|
||||
}
|
||||
.top-sites-header h2 { font-size: 1rem; font-weight: 700; color: var(--text); opacity: .9; }
|
||||
.link-btn {
|
||||
background: none; border: none; color: var(--accent); cursor: pointer; font-size: .9rem;
|
||||
}
|
||||
.link-btn:hover { color: var(--primary); text-decoration: underline; }
|
||||
|
||||
/* Individual bookmark tile */
|
||||
.bookmark {
|
||||
background: color-mix(in srgb, var(--text) 5%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--text) 10%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--bg) 30%, transparent);
|
||||
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 color-mix(in srgb, var(--bg) 50%, transparent);
|
||||
}
|
||||
|
||||
.bookmark-icon {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
/* accentuate icons & add-button */
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Favicon image in bookmark tile */
|
||||
.bookmark-favicon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 0.25rem;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
filter: drop-shadow(0 0 2px rgba(0,0,0,0.4));
|
||||
}
|
||||
|
||||
/* SVG icons in picker grid */
|
||||
.icon-item .grid-svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Icon category navigation */
|
||||
.icon-categories-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin: 0.25rem 0 0.75rem;
|
||||
}
|
||||
.icon-picker-layout { display:flex; gap:1rem; align-items:stretch; background:#f6f7f9; border:1px solid #e2e5ea; border-radius:14px; padding:0.85rem 0.85rem 0.85rem 0.75rem; box-shadow:inset 0 0 0 1px #ffffff, 0 2px 4px rgba(0,0,0,0.05); min-height:280px; max-height:320px; overflow:hidden; }
|
||||
.icon-side-nav { width:200px; display:flex; flex-direction:column; gap:0.3rem; overflow-y:auto; padding:0.3rem; background:linear-gradient(180deg,#fff,#f4f5f7); border:1px solid #d9dde2; border-radius:10px; max-height:100%; }
|
||||
.icon-main { flex:1; min-width:0; display:flex; flex-direction:column; }
|
||||
.icon-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem; }
|
||||
.icon-filter-label { font-size:0.875rem; color:#555; font-weight:600; }
|
||||
.favicon-toggle { display:flex; align-items:center; gap:0.4rem; }
|
||||
.favicon-checkbox { width:16px; height:16px; accent-color:var(--accent); }
|
||||
.favicon-label { font-size:0.75rem; color:#666; cursor:pointer; user-select:none; }
|
||||
.icon-filter { margin-bottom:0.5rem; background:#fff; border:1px solid #d4d9df; border-radius:8px; padding:0.5rem 0.7rem; font-size:0.8rem; }
|
||||
.icon-filter:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 2px rgba(123,46,255,0.25); }
|
||||
.icon-cat-btn { width:100%; text-align:left; padding:0.55rem 0.65rem 0.55rem 0.55rem; font-size:0.7rem; font-weight:600; letter-spacing:.5px; background:transparent; border:1px solid transparent; border-radius:8px; cursor:pointer; color:#4a4f55; display:flex; align-items:center; gap:0.55rem; transition: background .18s, color .18s, border-color .2s; position:relative; }
|
||||
.icon-cat-btn .material-symbols-outlined { font-size:18px; width:30px; height:30px; background:#e9edf1; border:1px solid #d4d9df; border-radius:8px; flex-shrink:0; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 1px #fff inset; }
|
||||
.icon-cat-btn::before { display:none; }
|
||||
.icon-cat-btn:hover { background:#eef2f6; }
|
||||
.icon-cat-btn.active { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:rgba(255,255,255,0.35); box-shadow:0 2px 6px -2px rgba(0,0,0,0.35); }
|
||||
.icon-cat-btn.active .material-symbols-outlined { background:rgba(255,255,255,0.22); border-color:rgba(255,255,255,0.4); color:#fff; }
|
||||
|
||||
.icon-section-label { display:none; }
|
||||
.icon-section-anchor { height:1px; width:100%; margin-top:4px; }
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Dynamic bookmark icon colors based on theme */
|
||||
.bookmark .material-symbols-outlined {
|
||||
color: var(--text, #E0E0E0) !important;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Ensure dark theme compatibility - fallback rules */
|
||||
body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
||||
.bookmark .material-symbols-outlined[style*="color: white"] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Add button style */
|
||||
.add-bookmark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 20px;
|
||||
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: color-mix(in srgb, var(--bg) 80%, transparent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Ensure popup overlays search bar and other UI (search-container has z-index:300) */
|
||||
z-index: 1005;
|
||||
backdrop-filter: blur(4px); /* add subtle blur behind the overlay */
|
||||
}
|
||||
|
||||
.popup.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Popup inner as white Material card */
|
||||
.popup-inner { display:flex; flex-direction:column; gap:1.1rem; color:#222; min-width:400px; background:#ffffff; border-radius:16px; box-shadow:0 12px 40px -8px rgba(0,0,0,0.35), 0 0 0 1px rgba(0,0,0,0.06); padding:1.5rem 1.5rem 1.25rem; transition:transform .35s cubic-bezier(.16,.84,.44,1), opacity .35s; transform:translateY(14px) scale(.94); opacity:0; width:760px; max-width:90vw; max-height:85vh; overflow:hidden; }
|
||||
|
||||
/* animate in when not hidden */
|
||||
.popup:not(.hidden) .popup-inner { transform:translateY(0) scale(1); opacity:1; }
|
||||
|
||||
/* dialog title */
|
||||
.popup-inner h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
/* field labels */
|
||||
.popup-inner label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
/* text/url/icon inputs */
|
||||
.popup-inner input[type="text"],
|
||||
.popup-inner input[type="url"] {
|
||||
width: 100%;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
color: #222222;
|
||||
margin-bottom: 1rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.popup-inner input[type="text"]:focus,
|
||||
.popup-inner input[type="url"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(123,46,255,0.2);
|
||||
}
|
||||
|
||||
/* Removed earlier duplicate icon-grid + icon-item block (consolidated below) */
|
||||
|
||||
/* action buttons container */
|
||||
.popup-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.popup-buttons button {
|
||||
min-width: 80px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Cancel button */
|
||||
#cancelBtn {
|
||||
background: #e0e0e0;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#cancelBtn:hover {
|
||||
background: #d5d5d5;
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
#saveBookmarkBtn {
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#saveBookmarkBtn:hover {
|
||||
background: #6a24e5;
|
||||
}
|
||||
|
||||
/* At a glance widget */
|
||||
.glance { position: fixed; right: 22px; bottom: 22px; }
|
||||
.glance.pos-br { right:22px; bottom:22px; left:auto; top:auto; }
|
||||
.glance.pos-bl { left:22px; bottom:22px; right:auto; top:auto; }
|
||||
.glance.pos-tr { right:22px; top:22px; left:auto; bottom:auto; }
|
||||
.glance.pos-tl { left:22px; top:22px; right:auto; bottom:auto; }
|
||||
.glance-card {
|
||||
min-width: 280px; background: color-mix(in srgb, var(--bg) 55%, transparent); border: 1px solid color-mix(in srgb, var(--text) 10%, transparent);
|
||||
border-radius: 16px; padding: 1rem; box-shadow: 0 14px 40px -18px color-mix(in srgb, var(--bg) 80%, transparent), inset 0 1px 0 color-mix(in srgb, var(--text) 5%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.glance { transition: transform 0.06s linear; will-change: transform; }
|
||||
.glance.dragging { transform: translate3d(var(--drag-x, 0px), var(--drag-y, 0px), 0) scale(1.02); }
|
||||
.glance.dragging .glance-card { box-shadow: 0 24px 60px -24px color-mix(in srgb, var(--bg) 90%, transparent), 0 0 0 2px color-mix(in srgb, var(--text) 12%, transparent) inset; }
|
||||
.glance-title { font-size: .95rem; color: var(--text); opacity: .9; margin-bottom: .65rem; }
|
||||
.glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
||||
.glance-tile { background: color-mix(in srgb, var(--text) 5%, transparent); border: 1px solid color-mix(in srgb, var(--text) 10%, transparent); border-radius: 12px; padding: .6rem .75rem; text-align:left; }
|
||||
.glance-label { font-size: .7rem; color: var(--accent); opacity: .85; margin-bottom: .25rem; }
|
||||
.glance-value { font-size: 1.05rem; letter-spacing: .3px; color: var(--text); }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.glance { position: static; margin-top: 1rem; }
|
||||
}
|
||||
|
||||
/* Color Palette */
|
||||
:root {
|
||||
--bg: #121418;
|
||||
--dark-blue: #0B1C2B;
|
||||
--dark-purple: #1B1035;
|
||||
--primary: #7B2EFF;
|
||||
--accent: #00C6FF;
|
||||
--text: #E0E0E0;
|
||||
}
|
||||
|
||||
/* Unified icon grid styling */
|
||||
.icon-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(42px,1fr)); gap:5px; flex:1 1 auto; min-height:160px; max-height:260px; overflow-y:auto; overflow-x:hidden; padding:0.6rem 0.6rem 0.7rem; background:rgba(255,255,255,0.45); backdrop-filter:blur(6px); border:1px solid rgba(0,0,0,0.08); border-radius:10px; position:relative; scroll-behavior:smooth; }
|
||||
.icon-main { flex:1; min-height:300px; }
|
||||
.icon-item { cursor:pointer; padding:6px; border:1px solid rgba(255,255,255,0.06); border-radius:10px; text-align:center; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,0.05); transition: background .15s, transform .15s, box-shadow .2s; font-size:0.65rem; line-height:1; font-weight:500; position:relative; }
|
||||
.icon-item::after { content:""; position:absolute; inset:0; border-radius:inherit; box-shadow:0 0 0 0 rgba(0,0,0,0); transition:box-shadow .25s; }
|
||||
.icon-item:hover { background:rgba(255,255,255,0.12); }
|
||||
.icon-item:active { transform:scale(.92); }
|
||||
.icon-item.selected { background:linear-gradient(135deg,var(--accent),var(--primary)); color:#fff; border-color:transparent; box-shadow:0 4px 10px -3px rgba(0,0,0,.6); }
|
||||
.icon-item.selected::after { box-shadow:0 0 0 2px rgba(255,255,255,0.65); }
|
||||
|
||||
/* Icon set selector row */
|
||||
.icon-set-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
.icon-section-label { display:none; }
|
||||
@@ -0,0 +1,66 @@
|
||||
:root {
|
||||
--bg: #0b0d10;
|
||||
--primary: #7b2eff;
|
||||
--accent: #00c6ff;
|
||||
--text: #e0e0e0;
|
||||
--url-bar-bg: #1c2030;
|
||||
--url-bar-border: #3e4652;
|
||||
--shadow-1: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||
--blur: 12px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
}
|
||||
|
||||
#menu-popup {
|
||||
background: color-mix(in srgb, var(--url-bar-bg) 92%, var(--text) 8%);
|
||||
border: 1px solid color-mix(in srgb, var(--primary) 25%, color-mix(in srgb, var(--accent) 18%, transparent));
|
||||
border-radius: 14px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 220px;
|
||||
box-shadow: var(--shadow-1);
|
||||
-webkit-backdrop-filter: blur(var(--blur));
|
||||
backdrop-filter: blur(var(--blur));
|
||||
}
|
||||
|
||||
#menu-popup button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease, filter 120ms ease;
|
||||
}
|
||||
|
||||
#menu-popup button:hover {
|
||||
background: color-mix(in srgb, var(--text) 8%, transparent);
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.zoom-controls button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#zoom-percent {
|
||||
min-width: 54px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/* Performance optimizations for renderer CSS - GPU Error 18 compatible */
|
||||
|
||||
/* Conservative hardware acceleration for animations */
|
||||
.tab, .bookmark, .icon-item {
|
||||
/* Only enable will-change when actually needed */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.tab:hover, .bookmark:hover, .icon-item:hover {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.tab:not(:hover), .bookmark:not(:hover), .icon-item:not(:hover) {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* Optimize scrolling - more conservative approach */
|
||||
#webviews, #bookmarkList, #iconGrid {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Use layout containment only, avoid paint containment which can cause GPU issues */
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* Use CSS containment for better performance - conservative approach */
|
||||
.tab-content {
|
||||
contain: layout style;
|
||||
}
|
||||
|
||||
/* Optimize transitions - reduced complexity */
|
||||
.tab {
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Reduce paint areas - more conservative transforms */
|
||||
.tab:hover, .bookmark:hover {
|
||||
transform: scale(1.01); /* Reduced scale to minimize GPU load */
|
||||
}
|
||||
|
||||
/* Use efficient selectors */
|
||||
.material-symbols-outlined {
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* Optimize text rendering - conservative settings */
|
||||
body {
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Conditional subpixel rendering for retina displays */
|
||||
@media (-webkit-min-device-pixel-ratio: 2) {
|
||||
body {
|
||||
-webkit-font-smoothing: subpixel-antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
/* Additional GPU-safe optimizations */
|
||||
* {
|
||||
/* Prevent unnecessary repaints */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Safe animation performance */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
/* existing styles */
|
||||
|
||||
/* Plugins panel */
|
||||
.plugins-list { display: grid; gap: 10px; }
|
||||
.plugin-item { display:flex; justify-content:space-between; align-items:center; border:1px solid rgba(255,255,255,0.12); padding:10px; border-radius:8px; background: rgba(255,255,255,0.03); }
|
||||
.plugin-meta { display:flex; flex-direction:column; gap:2px; min-width:0; }
|
||||
.plugin-title { font-weight:600; }
|
||||
.plugin-desc { opacity:.8; font-size:.9em; }
|
||||
.plugin-actions { display:flex; gap:8px; align-items:center; }
|
||||
.plugin-actions .spacer { width:8px; }
|
||||
.plugin-tags { display:flex; flex-wrap: wrap; gap:6px; margin-top: 4px; }
|
||||
.plugin-tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:999px; font-size:.75em; opacity:.9; border:1px solid rgba(255,255,255,0.16); background: rgba(255,255,255,0.06); }
|
||||
.plugin-authors { margin-top: 4px; font-size:.85em; opacity:.85; }
|
||||
.plugin-authors .muted { opacity:.7; margin-right: 6px; }
|
||||
:root {
|
||||
--bg: #121418;
|
||||
--gradient-end: #1B1035;
|
||||
--surface: rgba(255, 255, 255, 0.06);
|
||||
--surface-hover: rgba(255, 255, 255, 0.10);
|
||||
--primary: #7B2EFF;
|
||||
--primary-hover: #9654FF;
|
||||
--accent: #00C6FF;
|
||||
--text: #E0E0E0;
|
||||
--text-secondary: #B8B8C0;
|
||||
--text-muted: #8f8f9d;
|
||||
--border: rgba(255, 255, 255, 0.12);
|
||||
--border-subtle: rgba(255, 255, 255, 0.06);
|
||||
--ring: 0 0 0 2px rgba(123, 46, 255, 0.4);
|
||||
--glow-subtle: 0 4px 20px rgba(123, 46, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Load InterVariable */
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url('../assets/fonts/InterVariable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(800px 400px at 10% 0%, rgba(123, 46, 255, 0.08), transparent 60%),
|
||||
radial-gradient(800px 400px at 100% 20%, rgba(0, 198, 255, 0.06), transparent 60%),
|
||||
linear-gradient(180deg, var(--bg), var(--gradient-end));
|
||||
color: var(--text);
|
||||
font-family: 'InterVariable', system-ui, -apple-system, 'Segoe UI', 'Roboto', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
background: var(--bg);
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Subtle animated sheen around the container */
|
||||
|
||||
|
||||
/* Sidebar + content layout */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02));
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 1.5rem 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar h1 {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 300;
|
||||
margin: 0 0 1.5rem 0;
|
||||
padding: 0 1rem;
|
||||
color: var(--primary);
|
||||
letter-spacing: -0.01em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 10px 1rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.tab-link:hover {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-link.active {
|
||||
background: linear-gradient(90deg, rgba(123, 46, 255, 0.12), rgba(0, 198, 255, 0.08));
|
||||
color: var(--text);
|
||||
border-left-color: var(--primary);
|
||||
font-weight: 500;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 2rem 3rem;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.setting-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 400;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="file"] {
|
||||
padding: 0.5rem 0.65rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
input[type="text"]:focus,
|
||||
input[type="file"]:focus {
|
||||
box-shadow: var(--ring);
|
||||
border-color: var(--primary);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.5rem 0.65rem;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
box-shadow: var(--ring);
|
||||
border-color: var(--primary);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 14px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--surface-hover);
|
||||
box-shadow: var(--glow-subtle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Primary button style (e.g., Big Picture Mode) */
|
||||
.primary-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
background: var(--primary);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: var(--glow-subtle);
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background: var(--primary-hover);
|
||||
box-shadow: 0 4px 24px rgba(123, 46, 255, 0.25);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.note {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.35rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
font-size: 14px;
|
||||
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;
|
||||
}
|
||||
|
||||
.setting-group .setting-row button,
|
||||
.setting-group .setting-row input,
|
||||
.setting-group .setting-row select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Inline layout helpers (Firefox-like settings rows) */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.setting-row label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-row .note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.label-min {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.setting-row button,
|
||||
.setting-row input,
|
||||
.setting-row select {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.setting-row select {
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.range-row input[type="range"] {
|
||||
flex: 1 1 auto;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.range-row .range-value {
|
||||
min-width: 56px;
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: color-mix(in srgb, var(--text) 85%, transparent);
|
||||
}
|
||||
|
||||
/* Zoom controls */
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-radius: 6px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.zoom-value {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.zoom-presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.zoom-preset-btn {
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-preset-btn:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--primary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.zoom-preset-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: var(--glow-subtle);
|
||||
}
|
||||
|
||||
.zoom-preset-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.settings-fieldset {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
background: rgba(123, 46, 255, 0.03);
|
||||
}
|
||||
|
||||
.settings-fieldset legend {
|
||||
padding: 0 0.5rem;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Cards (customization groups) */
|
||||
.customization-group {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.customization-group > h3 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
position: relative;
|
||||
padding-left: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.setting-group > h3 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
padding-left: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Section titles */
|
||||
h2 {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.01em;
|
||||
position: relative;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
h2::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Theming: theme selector buttons override */
|
||||
.theme-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 8px;
|
||||
border: 2px solid var(--border) !important;
|
||||
border-radius: 8px !important;
|
||||
background: var(--surface) !important;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
min-height: 90px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.theme-btn:hover {
|
||||
background: var(--surface-hover) !important;
|
||||
border-color: var(--text-muted) !important;
|
||||
}
|
||||
.theme-btn.active {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 2px var(--primary), var(--glow-subtle);
|
||||
background: linear-gradient(180deg, rgba(123, 46, 255, 0.08), rgba(0, 198, 255, 0.05)) !important;
|
||||
}
|
||||
.theme-preview {
|
||||
width: 64px;
|
||||
height: 42px;
|
||||
border-radius: 4px !important;
|
||||
border: 1px solid var(--border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.custom-theme-btn { border-style: dashed !important; opacity: 0.95; }
|
||||
.custom-theme-btn:hover { opacity: 1; }
|
||||
|
||||
/* Range sliders */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
input[type="range"]:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Checkboxes/radios */
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
accent-color: var(--primary);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Layout & logo options */
|
||||
.layout-options { display: flex; flex-direction: column; gap: 10px; }
|
||||
.layout-options label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.layout-options label:hover { background: var(--surface); }
|
||||
.logo-options { display: flex; flex-direction: column; gap: 12px; }
|
||||
.logo-options label { display: flex; align-items: center; gap: 8px; }
|
||||
.logo-options input[type="text"] { flex: 1; }
|
||||
|
||||
/* Color customization controls */
|
||||
.color-controls { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
|
||||
.color-group { display: flex; flex-direction: column; gap: 8px; }
|
||||
.color-group label { font-size: 14px; color: var(--text-secondary); font-weight: 500; }
|
||||
input[type="color"] {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Preview area */
|
||||
.preview-container {
|
||||
background: var(--surface) !important;
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
background: var(--bg);
|
||||
border-radius: 4px;
|
||||
min-height: 200px;
|
||||
border: 1px dashed var(--border);
|
||||
}
|
||||
.preview-text { letter-spacing: 0.3px; }
|
||||
.preview-logo { font-size: 1.5rem; font-weight: 700; color: var(--primary); }
|
||||
.preview-search { width: 60%; height: 40px; background: rgba(255,255,255,0.1); border-radius: 20px; border: 1px solid rgba(255,255,255,0.2); }
|
||||
.preview-bookmarks { display: flex; gap: 10px; }
|
||||
.preview-bookmark { width: 50px; height: 50px; background: var(--accent); border-radius: 8px; }
|
||||
|
||||
/* History lists */
|
||||
#search-history-list, #site-history-list {
|
||||
padding: 0;
|
||||
margin: 6px 0 0 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
#search-history-list li, #site-history-list li {
|
||||
list-style: none;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
#site-history-list a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
#site-history-list a:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
/* About buttons */
|
||||
.github-btn, .help-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 4px !important;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
.github-btn:hover, .help-btn:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
.github-btn svg, .help-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
.about-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
|
||||
/* Debug info */
|
||||
.debug-info {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* General lists inside cards */
|
||||
.customization-group ul { list-style: none; padding: 0; margin: 0; }
|
||||
.customization-group ul li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.customization-group ul li:last-child { border-bottom: none; }
|
||||
|
||||
/* Theme management buttons */
|
||||
.theme-management { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
#reset-to-default {
|
||||
background: #d41b2c;
|
||||
border-color: #d41b2c;
|
||||
color: white;
|
||||
}
|
||||
#reset-to-default:hover {
|
||||
background: #a4161a;
|
||||
border-color: #a4161a;
|
||||
}
|
||||
|
||||
/* Scrollbar styling (Chromium) */
|
||||
*::-webkit-scrollbar { height: 12px; width: 12px; }
|
||||
*::-webkit-scrollbar-track { background: var(--bg); }
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, var(--primary), var(--accent));
|
||||
border-radius: 6px;
|
||||
border: 2px solid var(--bg);
|
||||
}
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, var(--primary-hover), var(--accent));
|
||||
}
|
||||
|
||||
/* small-screen adjustments */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem;
|
||||
}
|
||||
.tabs {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.tab-link {
|
||||
flex: 1 1 auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
.content {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
/* Load InterVariable Font */
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url('../assets/fonts/InterVariable.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* CSS Custom Properties */
|
||||
:root {
|
||||
--bg: #121418;
|
||||
--dark-blue: #0B1C2B;
|
||||
--dark-purple: #1B1035;
|
||||
--primary: #7B2EFF;
|
||||
--primary-rgb: 123, 46, 255;
|
||||
--accent: #00C6FF;
|
||||
--accent-rgb: 0, 198, 255;
|
||||
--text: #E0E0E0;
|
||||
--text-secondary: #A0A0A0;
|
||||
--card-bg: rgba(255, 255, 255, 0.05);
|
||||
--card-hover: rgba(255, 255, 255, 0.08);
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--success: #4CAF50;
|
||||
--success-rgb: 76, 175, 80;
|
||||
--warning: #FF9800;
|
||||
--warning-rgb: 255, 152, 0;
|
||||
--gradient-primary: linear-gradient(135deg, var(--accent), var(--primary));
|
||||
--gradient-bg: linear-gradient(145deg, var(--bg) 0%, var(--dark-purple) 100%);
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: var(--gradient-bg);
|
||||
color: var(--text);
|
||||
font-family: 'InterVariable', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Setup Container */
|
||||
.setup-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active,
|
||||
.progress-step.completed {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-bg);
|
||||
border: 2px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active .step-circle {
|
||||
background: var(--primary);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.4);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.progress-step.completed .step-circle {
|
||||
background: var(--success);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.progress-step.active .step-label {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--border);
|
||||
margin: 0 0.5rem;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* Setup Steps */
|
||||
.setup-step {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
flex: 1;
|
||||
animation: fadeIn 0.4s ease;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.setup-step.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.step-content {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.setup-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.setup-logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
filter: drop-shadow(0 8px 24px rgba(var(--primary-rgb), 0.3));
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Feature Grid */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-item:hover {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-item h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.feature-item p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Theme Grid */
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0 2rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
background: var(--card-bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.theme-card.selected {
|
||||
border-color: var(--primary);
|
||||
background: var(--card-hover);
|
||||
box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.2);
|
||||
}
|
||||
|
||||
.theme-card.selected::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.theme-color {
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-card:hover .theme-color {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.theme-description {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Default Browser Section */
|
||||
.default-browser-section {
|
||||
max-width: 500px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.default-browser-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.browser-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.default-browser-card h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.default-browser-card p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.default-browser-status {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.default-browser-status.checking .status-icon {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.default-browser-status.is-default {
|
||||
border-color: var(--success);
|
||||
background: rgba(var(--success-rgb), 0.1);
|
||||
}
|
||||
|
||||
.default-browser-status.is-default .status-icon {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.default-browser-status.is-default .status-text {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.default-browser-status.not-default {
|
||||
border-color: var(--warning);
|
||||
background: rgba(var(--warning-rgb), 0.1);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.default-browser-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Success Icon */
|
||||
.success-icon {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto 2rem;
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 4rem;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8px 32px rgba(var(--primary-rgb), 0.4);
|
||||
animation: scaleIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Completion Summary */
|
||||
.completion-summary {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin: 2rem auto;
|
||||
max-width: 500px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.summary-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Future Feature Teaser */
|
||||
.future-feature-teaser {
|
||||
background: linear-gradient(135deg, rgba(var(--primary-rgb), 0.1), rgba(var(--accent-rgb), 0.1));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin: 2rem auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.future-feature-teaser h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.teaser-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.teaser-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.step-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
margin-top: auto;
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.875rem 2rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
font-family: 'InterVariable', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--card-bg);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 1.125rem 2.5rem;
|
||||
font-size: 1.125rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.setup-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.progress-line {
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.setup-subtitle {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
flex-direction: column-reverse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
.progress-bar {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.setup-subtitle {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
:root {
|
||||
--bg: #0b0d10;
|
||||
--surface: #151923;
|
||||
--surface-strong: #1d2433;
|
||||
--text: #e8e8f0;
|
||||
--muted: #a4a7b3;
|
||||
--accent: #7b2eff;
|
||||
--accent-2: #00c6ff;
|
||||
--outline: #2b3040;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "InterVariable";
|
||||
src: url("../assets/fonts/InterVariable.ttf") format("truetype");
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(123, 46, 255, 0.25), transparent 35%),
|
||||
radial-gradient(circle at 80% 80%, rgba(0, 198, 255, 0.14), transparent 35%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "InterVariable", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.cef-shell {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.cef-start {
|
||||
width: min(760px, 100%);
|
||||
}
|
||||
|
||||
.cef-card {
|
||||
border: 1px solid color-mix(in srgb, var(--outline) 85%, white 15%);
|
||||
border-radius: 28px;
|
||||
padding: clamp(28px, 6vw, 56px);
|
||||
background: color-mix(in srgb, var(--surface) 90%, transparent);
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.38);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 12px;
|
||||
color: var(--accent-2);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.4rem, 8vw, 5rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 58ch;
|
||||
margin: 20px 0 30px;
|
||||
color: var(--muted);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.start-search {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--outline);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-strong);
|
||||
}
|
||||
|
||||
.start-search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0 16px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.start-search input::placeholder {
|
||||
color: color-mix(in srgb, var(--muted) 75%, transparent);
|
||||
}
|
||||
|
||||
.start-search button,
|
||||
.quick-links a {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.start-search button {
|
||||
padding: 12px 22px;
|
||||
}
|
||||
|
||||
.quick-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.quick-links a {
|
||||
padding: 10px 16px;
|
||||
background: color-mix(in srgb, var(--surface-strong) 80%, white 8%);
|
||||
border: 1px solid var(--outline);
|
||||
}
|
||||
|
||||
.quick-links a:hover,
|
||||
.start-search button:hover {
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.cef-shell {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.start-search {
|
||||
border-radius: 20px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.start-search input,
|
||||
.start-search button {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
"home",
|
||||
"star",
|
||||
"bookmark",
|
||||
"favorite",
|
||||
"public",
|
||||
"search",
|
||||
"settings"
|
||||
// … add as many icon names as you like …
|
||||
]
|
||||
@@ -0,0 +1,849 @@
|
||||
/**
|
||||
* Browser Customization System
|
||||
* Allows users to customize themes, colors, and layouts non-destructively
|
||||
*/
|
||||
|
||||
class BrowserCustomizer {
|
||||
constructor() {
|
||||
this.defaultTheme = {
|
||||
name: 'Default',
|
||||
colors: {
|
||||
bg: '#121418',
|
||||
darkBlue: '#0B1C2B',
|
||||
darkPurple: '#1B1035',
|
||||
primary: '#7B2EFF',
|
||||
accent: '#00C6FF',
|
||||
text: '#E0E0E0',
|
||||
urlBarBg: '#1C2030',
|
||||
urlBarText: '#E0E0E0',
|
||||
urlBarBorder: '#3E4652',
|
||||
tabBg: '#161925',
|
||||
tabText: '#A4A7B3',
|
||||
tabActive: '#1C2030',
|
||||
tabActiveText: '#E0E0E0',
|
||||
tabBorder: '#2B3040'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #121418 0%, #1B1035 100%)'
|
||||
};
|
||||
|
||||
this.predefinedThemes = {
|
||||
default: this.defaultTheme,
|
||||
ocean: {
|
||||
name: 'Ocean',
|
||||
colors: {
|
||||
bg: '#1a365d',
|
||||
darkBlue: '#2a4365',
|
||||
darkPurple: '#2c5282',
|
||||
primary: '#3182ce',
|
||||
accent: '#00d9ff',
|
||||
text: '#e2e8f0',
|
||||
urlBarBg: '#2d5282',
|
||||
urlBarText: '#e2e8f0',
|
||||
urlBarBorder: '#1e3a5f',
|
||||
tabBg: '#2a4365',
|
||||
tabText: '#cbd5e0',
|
||||
tabActive: '#2d5282',
|
||||
tabActiveText: '#e2e8f0',
|
||||
tabBorder: '#1a365d'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #1a365d 0%, #2c5282 100%)'
|
||||
},
|
||||
forest: {
|
||||
name: 'Forest',
|
||||
colors: {
|
||||
bg: '#1a202c',
|
||||
darkBlue: '#2d3748',
|
||||
darkPurple: '#4a5568',
|
||||
primary: '#68d391',
|
||||
accent: '#9ae6b4',
|
||||
text: '#f7fafc',
|
||||
urlBarBg: '#2d3748',
|
||||
urlBarText: '#f7fafc',
|
||||
urlBarBorder: '#4a5568',
|
||||
tabBg: '#2d3748',
|
||||
tabText: '#cbd5e0',
|
||||
tabActive: '#4a5568',
|
||||
tabActiveText: '#f7fafc',
|
||||
tabBorder: '#1a202c'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #1a202c 0%, #2d3748 100%)'
|
||||
},
|
||||
sunset: {
|
||||
name: 'Sunset',
|
||||
colors: {
|
||||
bg: '#744210',
|
||||
darkBlue: '#975a16',
|
||||
darkPurple: '#c05621',
|
||||
primary: '#ed8936',
|
||||
accent: '#fbb040',
|
||||
text: '#fffaf0',
|
||||
urlBarBg: '#975a16',
|
||||
urlBarText: '#fffaf0',
|
||||
urlBarBorder: '#c05621',
|
||||
tabBg: '#975a16',
|
||||
tabText: '#fde4b6',
|
||||
tabActive: '#c05621',
|
||||
tabActiveText: '#fffaf0',
|
||||
tabBorder: '#744210'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #744210 0%, #c05621 100%)'
|
||||
},
|
||||
cyberpunk: {
|
||||
name: 'Cyberpunk Neon',
|
||||
colors: {
|
||||
bg: '#0a0a0a',
|
||||
darkBlue: '#1a0520',
|
||||
darkPurple: '#2a0a3a',
|
||||
primary: '#ff0080',
|
||||
accent: '#00ffff',
|
||||
text: '#ffffff',
|
||||
urlBarBg: '#1a0520',
|
||||
urlBarText: '#ffffff',
|
||||
urlBarBorder: '#ff0080',
|
||||
tabBg: '#1a0520',
|
||||
tabText: '#00ffff',
|
||||
tabActive: '#2a0a3a',
|
||||
tabActiveText: '#ff0080',
|
||||
tabBorder: '#ff0080'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #0a0a0a 0%, #2a0a3a 50%, #1a0520 100%)'
|
||||
},
|
||||
'midnight-rose': {
|
||||
name: 'Midnight Rose',
|
||||
colors: {
|
||||
bg: '#1c1820',
|
||||
darkBlue: '#2d2433',
|
||||
darkPurple: '#3d3046',
|
||||
primary: '#d4af37',
|
||||
accent: '#ffd700',
|
||||
text: '#f5f5dc',
|
||||
urlBarBg: '#3d3046',
|
||||
urlBarText: '#f5f5dc',
|
||||
urlBarBorder: '#d4af37',
|
||||
tabBg: '#2d2433',
|
||||
tabText: '#d4af37',
|
||||
tabActive: '#3d3046',
|
||||
tabActiveText: '#ffd700',
|
||||
tabBorder: '#1c1820'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #1c1820 0%, #3d3046 100%)'
|
||||
},
|
||||
'arctic-ice': {
|
||||
name: 'Arctic Ice',
|
||||
colors: {
|
||||
bg: '#f0f8ff',
|
||||
darkBlue: '#e6f3ff',
|
||||
darkPurple: '#d1e7ff',
|
||||
primary: '#4169e1',
|
||||
accent: '#87ceeb',
|
||||
text: '#2f4f4f',
|
||||
urlBarBg: '#e6f3ff',
|
||||
urlBarText: '#2f4f4f',
|
||||
urlBarBorder: '#4169e1',
|
||||
tabBg: '#e6f3ff',
|
||||
tabText: '#4169e1',
|
||||
tabActive: '#d1e7ff',
|
||||
tabActiveText: '#2f4f4f',
|
||||
tabBorder: '#f0f8ff'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #f0f8ff 0%, #d1e7ff 100%)'
|
||||
},
|
||||
'cherry-blossom': {
|
||||
name: 'Cherry Blossom',
|
||||
colors: {
|
||||
bg: '#fff5f8',
|
||||
darkBlue: '#ffe4e8',
|
||||
darkPurple: '#ffd4db',
|
||||
primary: '#ff69b4',
|
||||
accent: '#ffb6c1',
|
||||
text: '#8b4513',
|
||||
urlBarBg: '#ffe4e8',
|
||||
urlBarText: '#8b4513',
|
||||
urlBarBorder: '#ff69b4',
|
||||
tabBg: '#ffe4e8',
|
||||
tabText: '#ff69b4',
|
||||
tabActive: '#ffd4db',
|
||||
tabActiveText: '#8b4513',
|
||||
tabBorder: '#fff5f8'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #fff5f8 0%, #ffd4db 100%)'
|
||||
},
|
||||
'cosmic-purple': {
|
||||
name: 'Cosmic Purple',
|
||||
colors: {
|
||||
bg: '#0f0524',
|
||||
darkBlue: '#1a0b3d',
|
||||
darkPurple: '#2d1b69',
|
||||
primary: '#8a2be2',
|
||||
accent: '#da70d6',
|
||||
text: '#e6e6fa',
|
||||
urlBarBg: '#1a0b3d',
|
||||
urlBarText: '#e6e6fa',
|
||||
urlBarBorder: '#8a2be2',
|
||||
tabBg: '#1a0b3d',
|
||||
tabText: '#da70d6',
|
||||
tabActive: '#2d1b69',
|
||||
tabActiveText: '#e6e6fa',
|
||||
tabBorder: '#0f0524'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #0f0524 0%, #2d1b69 50%, #4b0082 100%)'
|
||||
},
|
||||
'emerald-dream': {
|
||||
name: 'Emerald Dream',
|
||||
colors: {
|
||||
bg: '#0d2818',
|
||||
darkBlue: '#1a3a2e',
|
||||
darkPurple: '#2d5a44',
|
||||
primary: '#50c878',
|
||||
accent: '#98fb98',
|
||||
text: '#f0fff0',
|
||||
urlBarBg: '#1a3a2e',
|
||||
urlBarText: '#f0fff0',
|
||||
urlBarBorder: '#50c878',
|
||||
tabBg: '#1a3a2e',
|
||||
tabText: '#98fb98',
|
||||
tabActive: '#2d5a44',
|
||||
tabActiveText: '#f0fff0',
|
||||
tabBorder: '#0d2818'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #0d2818 0%, #2d5a44 100%)'
|
||||
},
|
||||
'mocha-coffee': {
|
||||
name: 'Mocha Coffee',
|
||||
colors: {
|
||||
bg: '#3c2414',
|
||||
darkBlue: '#4a2c1a',
|
||||
darkPurple: '#5d3a26',
|
||||
primary: '#d2691e',
|
||||
accent: '#daa520',
|
||||
text: '#faf0e6',
|
||||
urlBarBg: '#4a2c1a',
|
||||
urlBarText: '#faf0e6',
|
||||
urlBarBorder: '#d2691e',
|
||||
tabBg: '#4a2c1a',
|
||||
tabText: '#daa520',
|
||||
tabActive: '#5d3a26',
|
||||
tabActiveText: '#faf0e6',
|
||||
tabBorder: '#3c2414'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #3c2414 0%, #5d3a26 100%)'
|
||||
},
|
||||
'lavender-fields': {
|
||||
name: 'Lavender Fields',
|
||||
colors: {
|
||||
bg: '#f8f4ff',
|
||||
darkBlue: '#ede4ff',
|
||||
darkPurple: '#e6d8ff',
|
||||
primary: '#9370db',
|
||||
accent: '#dda0dd',
|
||||
text: '#4b0082',
|
||||
urlBarBg: '#ede4ff',
|
||||
urlBarText: '#4b0082',
|
||||
urlBarBorder: '#9370db',
|
||||
tabBg: '#ede4ff',
|
||||
tabText: '#9370db',
|
||||
tabActive: '#e6d8ff',
|
||||
tabActiveText: '#4b0082',
|
||||
tabBorder: '#f8f4ff'
|
||||
},
|
||||
layout: 'centered',
|
||||
showLogo: true,
|
||||
customTitle: 'Nebula Browser',
|
||||
gradient: 'linear-gradient(145deg, #f8f4ff 0%, #e6d8ff 100%)'
|
||||
}
|
||||
};
|
||||
|
||||
this.currentTheme = this.loadTheme();
|
||||
this.activeThemeName = this.loadActiveThemeName();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.loadCurrentTheme();
|
||||
this.restoreActiveThemeButton();
|
||||
this.updatePreview();
|
||||
this.updateCustomThemeButton();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Theme preset buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const themeName = e.currentTarget.dataset.theme;
|
||||
this.applyPredefinedTheme(themeName);
|
||||
});
|
||||
});
|
||||
|
||||
// Color inputs
|
||||
const colorInputs = ['bg-color', 'gradient-color', 'accent-color', 'secondary-color', 'text-color'];
|
||||
colorInputs.forEach(inputId => {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.addEventListener('input', (e) => {
|
||||
this.updateColorFromInput(inputId, e.target.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Layout options
|
||||
document.querySelectorAll('input[name="layout"]').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
this.currentTheme.layout = e.target.value;
|
||||
|
||||
// Clear active theme name since this is now a custom theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
|
||||
// Remove active class from all theme buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
this.saveTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToPages();
|
||||
this.updateCustomThemeButton();
|
||||
});
|
||||
});
|
||||
|
||||
// Logo options
|
||||
const showLogoInput = document.getElementById('show-logo');
|
||||
if (showLogoInput) {
|
||||
showLogoInput.addEventListener('change', (e) => {
|
||||
this.currentTheme.showLogo = e.target.checked;
|
||||
|
||||
// Clear active theme name since this is now a custom theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
|
||||
// Remove active class from all theme buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
this.saveTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToPages();
|
||||
this.updateCustomThemeButton();
|
||||
});
|
||||
}
|
||||
|
||||
const customTitleInput = document.getElementById('custom-title');
|
||||
if (customTitleInput) {
|
||||
customTitleInput.addEventListener('input', (e) => {
|
||||
this.currentTheme.customTitle = e.target.value || 'Nebula Browser';
|
||||
|
||||
// Clear active theme name since this is now a custom theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
|
||||
// Remove active class from all theme buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
this.saveTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToPages();
|
||||
this.updateCustomThemeButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Theme management buttons
|
||||
this.setupThemeManagementButtons();
|
||||
}
|
||||
|
||||
setupThemeManagementButtons() {
|
||||
const saveBtn = document.getElementById('save-custom-theme');
|
||||
const exportBtn = document.getElementById('export-theme');
|
||||
const importBtn = document.getElementById('import-theme');
|
||||
const resetBtn = document.getElementById('reset-to-default');
|
||||
const fileInput = document.getElementById('theme-file-input');
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => this.saveCustomTheme());
|
||||
}
|
||||
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', () => this.exportTheme());
|
||||
}
|
||||
|
||||
if (importBtn) {
|
||||
importBtn.addEventListener('click', () => fileInput.click());
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', (e) => this.importTheme(e));
|
||||
}
|
||||
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => this.resetToDefault());
|
||||
}
|
||||
}
|
||||
|
||||
updateColorFromInput(inputId, color) {
|
||||
const colorMap = {
|
||||
'bg-color': 'bg',
|
||||
'gradient-color': 'darkPurple',
|
||||
'accent-color': 'primary',
|
||||
'secondary-color': 'accent',
|
||||
'text-color': 'text'
|
||||
};
|
||||
|
||||
const colorKey = colorMap[inputId];
|
||||
if (colorKey) {
|
||||
this.currentTheme.colors[colorKey] = color;
|
||||
|
||||
// Update gradient for background or gradient changes
|
||||
if (colorKey === 'bg' || colorKey === 'darkPurple') {
|
||||
this.currentTheme.gradient = `linear-gradient(145deg, ${this.currentTheme.colors.bg} 0%, ${this.currentTheme.colors.darkPurple} 100%)`;
|
||||
}
|
||||
|
||||
// Clear active theme name since this is now a custom theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
|
||||
// Remove active class from all theme buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
this.saveTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToPages();
|
||||
this.updateCustomThemeButton();
|
||||
}
|
||||
}
|
||||
|
||||
applyPredefinedTheme(themeName) {
|
||||
if (themeName === 'custom') {
|
||||
// For custom theme, just activate the button but don't change the current theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
this.updateThemeButtons('custom');
|
||||
this.updateCustomThemeButton();
|
||||
} else if (this.predefinedThemes[themeName]) {
|
||||
this.currentTheme = { ...this.predefinedThemes[themeName] };
|
||||
this.activeThemeName = themeName;
|
||||
this.saveTheme();
|
||||
this.saveActiveThemeName(themeName);
|
||||
this.loadCurrentTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToCurrentPage();
|
||||
this.applyThemeToPages();
|
||||
this.updateThemeButtons(themeName);
|
||||
this.updateCustomThemeButton();
|
||||
}
|
||||
}
|
||||
|
||||
updateThemeButtons(activeTheme) {
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.theme === activeTheme) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateCustomThemeButton() {
|
||||
const customBtn = document.getElementById('theme-custom');
|
||||
if (!customBtn) return;
|
||||
|
||||
// Check if current theme matches any predefined theme
|
||||
const matchingTheme = this.detectMatchingPredefinedTheme();
|
||||
const isCustomTheme = !matchingTheme;
|
||||
|
||||
if (isCustomTheme) {
|
||||
customBtn.style.display = 'flex';
|
||||
// Update the preview to show current colors
|
||||
const preview = customBtn.querySelector('.theme-preview');
|
||||
if (preview && this.currentTheme) {
|
||||
preview.style.background = this.currentTheme.gradient ||
|
||||
`linear-gradient(145deg, ${this.currentTheme.colors.bg}, ${this.currentTheme.colors.darkPurple})`;
|
||||
}
|
||||
// Set active theme name to custom if it's not already set to a predefined theme
|
||||
if (this.activeThemeName !== 'custom') {
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
}
|
||||
} else {
|
||||
customBtn.style.display = 'none';
|
||||
// If we found a matching predefined theme, update activeThemeName if it was set to custom
|
||||
if (this.activeThemeName === 'custom') {
|
||||
this.activeThemeName = matchingTheme;
|
||||
this.saveActiveThemeName(matchingTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadCurrentTheme() {
|
||||
// Update color inputs
|
||||
document.getElementById('bg-color').value = this.currentTheme.colors.bg;
|
||||
document.getElementById('gradient-color').value = this.currentTheme.colors.darkPurple;
|
||||
document.getElementById('accent-color').value = this.currentTheme.colors.primary;
|
||||
document.getElementById('secondary-color').value = this.currentTheme.colors.accent;
|
||||
document.getElementById('text-color').value = this.currentTheme.colors.text;
|
||||
|
||||
// Update layout radio
|
||||
const layoutInput = document.querySelector(`input[name="layout"][value="${this.currentTheme.layout}"]`);
|
||||
if (layoutInput) layoutInput.checked = true;
|
||||
|
||||
// Update logo options
|
||||
document.getElementById('show-logo').checked = this.currentTheme.showLogo;
|
||||
document.getElementById('custom-title').value = this.currentTheme.customTitle;
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
const preview = document.getElementById('preview-container');
|
||||
const previewHome = preview.querySelector('.preview-home');
|
||||
const previewLogo = preview.querySelector('.preview-logo');
|
||||
const previewText = preview.querySelector('.preview-text');
|
||||
|
||||
// Apply colors to preview
|
||||
previewHome.style.background = this.currentTheme.gradient;
|
||||
|
||||
// Handle logo visibility
|
||||
if (this.currentTheme.showLogo) {
|
||||
previewLogo.style.display = 'block';
|
||||
previewLogo.style.color = this.currentTheme.colors.primary;
|
||||
previewLogo.textContent = '🌌';
|
||||
} else {
|
||||
previewLogo.style.display = 'none';
|
||||
}
|
||||
|
||||
// Always show preview text with custom title
|
||||
if (previewText) {
|
||||
previewText.style.color = this.currentTheme.colors.primary;
|
||||
previewText.textContent = this.currentTheme.customTitle;
|
||||
}
|
||||
|
||||
// Update CSS custom properties for live preview
|
||||
this.applyThemeToCurrentPage();
|
||||
}
|
||||
|
||||
applyThemeToCurrentPage() {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--bg', this.currentTheme.colors.bg);
|
||||
root.style.setProperty('--dark-blue', this.currentTheme.colors.darkBlue);
|
||||
root.style.setProperty('--dark-purple', this.currentTheme.colors.darkPurple);
|
||||
root.style.setProperty('--primary', this.currentTheme.colors.primary);
|
||||
root.style.setProperty('--accent', this.currentTheme.colors.accent);
|
||||
root.style.setProperty('--text', this.currentTheme.colors.text);
|
||||
root.style.setProperty('--url-bar-bg', this.currentTheme.colors.urlBarBg);
|
||||
root.style.setProperty('--url-bar-text', this.currentTheme.colors.urlBarText);
|
||||
root.style.setProperty('--url-bar-border', this.currentTheme.colors.urlBarBorder);
|
||||
root.style.setProperty('--tab-bg', this.currentTheme.colors.tabBg);
|
||||
root.style.setProperty('--tab-text', this.currentTheme.colors.tabText);
|
||||
root.style.setProperty('--tab-active', this.currentTheme.colors.tabActive);
|
||||
root.style.setProperty('--tab-active-text', this.currentTheme.colors.tabActiveText);
|
||||
root.style.setProperty('--tab-border', this.currentTheme.colors.tabBorder);
|
||||
|
||||
// Apply gradient to body if it exists
|
||||
const body = document.body;
|
||||
if (body && this.currentTheme.gradient) {
|
||||
body.style.background = this.currentTheme.gradient;
|
||||
console.log('[THEME] Applied gradient:', this.currentTheme.gradient);
|
||||
}
|
||||
}
|
||||
|
||||
applyThemeToPages() {
|
||||
// This will be called to apply theme to home.html and other pages
|
||||
this.saveTheme();
|
||||
|
||||
// Send theme update to host (for settings webview)
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('theme-update', this.currentTheme);
|
||||
}
|
||||
// Fallback: send via postMessage (for iframe embedding)
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'theme-update',
|
||||
theme: this.currentTheme
|
||||
}, '*');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not send theme update to parent window');
|
||||
}
|
||||
}
|
||||
|
||||
saveCustomTheme() {
|
||||
const themeName = prompt('Enter a name for your custom theme:', 'My Custom Theme');
|
||||
if (themeName) {
|
||||
const customThemes = this.getCustomThemes();
|
||||
customThemes[themeName.toLowerCase().replace(/\s+/g, '-')] = {
|
||||
...this.currentTheme,
|
||||
name: themeName
|
||||
};
|
||||
localStorage.setItem('customThemes', JSON.stringify(customThemes));
|
||||
|
||||
// Show success message
|
||||
this.showMessage('Custom theme saved successfully!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
exportTheme() {
|
||||
const themeData = {
|
||||
...this.currentTheme,
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(themeData, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `nebula-theme-${themeData.name.toLowerCase().replace(/\s+/g, '-')}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
this.showMessage('Theme exported successfully!', 'success');
|
||||
}
|
||||
|
||||
importTheme(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const themeData = JSON.parse(e.target.result);
|
||||
|
||||
// Validate theme structure
|
||||
if (this.validateTheme(themeData)) {
|
||||
this.currentTheme = themeData;
|
||||
this.saveTheme();
|
||||
this.loadCurrentTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToCurrentPage();
|
||||
this.applyThemeToPages();
|
||||
this.showMessage('Theme imported successfully!', 'success');
|
||||
} else {
|
||||
this.showMessage('Invalid theme file format.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showMessage('Error reading theme file.', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
validateTheme(theme) {
|
||||
return theme &&
|
||||
theme.colors &&
|
||||
theme.colors.bg &&
|
||||
theme.colors.primary &&
|
||||
theme.colors.accent &&
|
||||
theme.colors.text;
|
||||
}
|
||||
|
||||
resetToDefault() {
|
||||
if (confirm('Are you sure you want to reset to the default theme? This will lose your current customizations.')) {
|
||||
this.currentTheme = { ...this.defaultTheme };
|
||||
this.activeThemeName = 'default';
|
||||
this.saveTheme();
|
||||
this.saveActiveThemeName('default');
|
||||
this.loadCurrentTheme();
|
||||
this.updatePreview();
|
||||
this.applyThemeToCurrentPage();
|
||||
this.applyThemeToPages();
|
||||
this.updateThemeButtons('default');
|
||||
this.showMessage('Theme reset to default.', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
saveTheme() {
|
||||
localStorage.setItem('currentTheme', JSON.stringify(this.currentTheme));
|
||||
}
|
||||
|
||||
loadTheme() {
|
||||
const savedTheme = localStorage.getItem('currentTheme');
|
||||
return savedTheme ? JSON.parse(savedTheme) : { ...this.defaultTheme };
|
||||
}
|
||||
|
||||
saveActiveThemeName(themeName) {
|
||||
localStorage.setItem('activeThemeName', themeName);
|
||||
}
|
||||
|
||||
loadActiveThemeName() {
|
||||
return localStorage.getItem('activeThemeName') || 'default';
|
||||
}
|
||||
|
||||
restoreActiveThemeButton() {
|
||||
// First, remove active class from all buttons
|
||||
document.querySelectorAll('.theme-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// If no active theme name is saved, try to detect which predefined theme matches current theme
|
||||
if (!this.activeThemeName) {
|
||||
this.activeThemeName = this.detectMatchingPredefinedTheme();
|
||||
if (this.activeThemeName) {
|
||||
this.saveActiveThemeName(this.activeThemeName);
|
||||
} else {
|
||||
// If no predefined theme matches, this is a custom theme
|
||||
this.activeThemeName = 'custom';
|
||||
this.saveActiveThemeName('custom');
|
||||
}
|
||||
}
|
||||
|
||||
// Update the custom theme button visibility
|
||||
this.updateCustomThemeButton();
|
||||
|
||||
// Then, add active class to the currently active theme button
|
||||
const activeBtn = document.querySelector(`[data-theme="${this.activeThemeName}"]`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
detectMatchingPredefinedTheme() {
|
||||
// Check if current theme matches any predefined theme
|
||||
for (const [themeName, themeData] of Object.entries(this.predefinedThemes)) {
|
||||
if (this.themesMatch(this.currentTheme, themeData)) {
|
||||
return themeName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
themesMatch(theme1, theme2) {
|
||||
// Compare essential properties to determine if themes match
|
||||
return theme1.colors.bg === theme2.colors.bg &&
|
||||
theme1.colors.darkPurple === theme2.colors.darkPurple &&
|
||||
theme1.colors.primary === theme2.colors.primary &&
|
||||
theme1.colors.accent === theme2.colors.accent &&
|
||||
theme1.colors.text === theme2.colors.text &&
|
||||
theme1.layout === theme2.layout &&
|
||||
theme1.showLogo === theme2.showLogo &&
|
||||
theme1.customTitle === theme2.customTitle;
|
||||
}
|
||||
|
||||
getCustomThemes() {
|
||||
const customThemes = localStorage.getItem('customThemes');
|
||||
return customThemes ? JSON.parse(customThemes) : {};
|
||||
}
|
||||
|
||||
showMessage(message, type = 'info') {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message message-${type}`;
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease;
|
||||
background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#e53e3e' : '#4299e1'};
|
||||
`;
|
||||
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
messageDiv.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => {
|
||||
if (messageDiv.parentNode) {
|
||||
messageDiv.parentNode.removeChild(messageDiv);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Static method to apply theme to any page
|
||||
static applyThemeToPage() {
|
||||
const savedTheme = localStorage.getItem('currentTheme');
|
||||
if (savedTheme) {
|
||||
const theme = JSON.parse(savedTheme);
|
||||
const root = document.documentElement;
|
||||
|
||||
root.style.setProperty('--bg', theme.colors.bg);
|
||||
root.style.setProperty('--dark-blue', theme.colors.darkBlue);
|
||||
root.style.setProperty('--dark-purple', theme.colors.darkPurple);
|
||||
root.style.setProperty('--primary', theme.colors.primary);
|
||||
root.style.setProperty('--accent', theme.colors.accent);
|
||||
root.style.setProperty('--text', theme.colors.text);
|
||||
root.style.setProperty('--url-bar-bg', theme.colors.urlBarBg);
|
||||
root.style.setProperty('--url-bar-text', theme.colors.urlBarText);
|
||||
root.style.setProperty('--url-bar-border', theme.colors.urlBarBorder);
|
||||
root.style.setProperty('--tab-bg', theme.colors.tabBg);
|
||||
root.style.setProperty('--tab-text', theme.colors.tabText);
|
||||
root.style.setProperty('--tab-active', theme.colors.tabActive);
|
||||
root.style.setProperty('--tab-active-text', theme.colors.tabActiveText);
|
||||
root.style.setProperty('--tab-border', theme.colors.tabBorder);
|
||||
|
||||
// Apply gradient to body if it exists
|
||||
const body = document.body;
|
||||
if (body && theme.gradient) {
|
||||
body.style.background = theme.gradient;
|
||||
console.log('[THEME] Applied gradient from storage:', theme.gradient);
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize on settings page
|
||||
if (window.location.pathname.includes('settings.html')) {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.browserCustomizer = new BrowserCustomizer();
|
||||
});
|
||||
}
|
||||
|
||||
// Add keyframe animations for messages
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from { transform: translateX(0); opacity: 1; }
|
||||
to { transform: translateX(100%); opacity: 0; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -0,0 +1,148 @@
|
||||
// Unified icon set loaders with graceful fallbacks.
|
||||
// Each loader returns an array of string icon names (NOT SVG markup) suitable for name-based selection.
|
||||
// Some libraries don't have an easy metadata endpoint; we attempt a fetch and fall back to a small curated subset.
|
||||
|
||||
import { fetchAllIcons as fetchMaterialIcons, icons as materialFallback } from './icons.js';
|
||||
|
||||
async function attemptJSON(url, transform) {
|
||||
try {
|
||||
const res = await fetch(url, { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||
const data = await res.json();
|
||||
return transform ? transform(data) : data;
|
||||
} catch (e) {
|
||||
console.warn('[IconSets] Failed to fetch', url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- SVG helpers ---
|
||||
async function attemptText(url) {
|
||||
try {
|
||||
const res = await fetch(url, { cache: 'force-cache' });
|
||||
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||
const txt = await res.text();
|
||||
if (!/^<svg[\s\S]*<\/svg>$/i.test(txt.trim())) throw new Error('Not SVG');
|
||||
return txt;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function svgToDataUrl(svg) {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg.replace(/<script[\s\S]*?<\/script>/gi, ''));
|
||||
}
|
||||
|
||||
const staticFallbacks = {
|
||||
lucide: ['activity','airplay','alarm-clock','align-center','anchor','apple','archive','arrow-big-up','at-sign','award','battery','bell','bluetooth','book','bookmark','briefcase','calendar','camera','cast','check','chevron-down','chrome','cloud','code','command','compass','cpu','database','download','edit','external-link','eye','file','folder','gamepad','globe','heart','help-circle','home','image','info','keyboard','layers','link','list','lock','mail','map','menu','mic','moon','music','package','pie-chart','play','plus','pocket','power','refresh-ccw','rss','save','scissors','search','settings','share','shield','smartphone','speaker','star','sun','tablet','tag','terminal','thumbs-up','trash','tv','twitter','upload','user','video','wifi','x','zap'],
|
||||
tabler: ['activity','alarm','affiliate','anchor','api','app-window','apple','archive','armchair','arrow-down','at','award','backspace','ballon','battery','bell','bluetooth','bolt','book','bookmark','briefcase','browser','bug','building','calendar','camera','car','chart-area','chart-bar','chart-pie','chart-scatter','check','chevron-down','cloud','code','coffee','color-swatch','command','compass','cpu','credit-card','dashboard','database','device-desktop','device-mobile','dice','dna','download','drop','edit','file','filter','flag','flame','folder','gift','globe','grid','hash','headphones','heart','help','home','id','inbox','info-circle','key','keyboard','language','layers','layout','layout-grid','letter-a','link','lock','login','logout','mail','map','menu','message','microphone','mood-happy','moon','music','news','note','package','password','phone','photo','player-play','plug','plus','power','printer','puzzle','refresh','rocket','route','rss','school','search','server','settings','share','shield','smart-home','snowflake','sparkles','star','sun','switch','tag','thumb-up','tool','trash','trophy','typography','upload','user','video','wifi','world','x'],
|
||||
phosphor: ['activity','airplane','anchor','apple-logo','archive','arrow-down','arrow-up','at','bag','bell','book','bookmark','bounding-box','briefcase','browser','bug','calendar','camera','car','check','clipboard','cloud','code','command','compass','cpu','credit-card','database','device-mobile','device-tablet','door','download','drop','envelope','eye','eyedropper','file','film-strip','flag','flame','folder','funnel','game-controller','gear','globe','hand','hash','headphones','heart','house','image','info','key','keyboard','leaf','link','lock','magnet','magnifying-glass','map-pin','microphone','moon','music-note','note','nut','package','paper-plane','paperclip','path','pen','phone','plug','plus','power','printer','question','rocket','rss','scissors','share','shield','shopping-cart','sketch-logo','smiley','sparkle','speaker-high','star','sun','swatches','tag','terminal','thumbs-up','toolbox','trash','trophy','tv','user','users','video-camera','wifi-high','x','yarn','youtube-logo','zap'],
|
||||
remix: ['add','alarm','alert','anchor','apps','archive','arrow-down','arrow-right','arrow-up','at','award','bank','bar-chart','battery','bell','bluetooth','book','bookmark','briefcase','bug','building','calendar','camera','car','chat','chrome','clipboard','cloud','code','command','compass','copyleft','copyright','cpu','dashboard','database','delete-bin','device','dice','download','dribbble','drive','earth','edge','edit','facebook','file','filter','fire','flag','folder','gamepad','gift','github','gitlab','global','google','group','hard-drive','heart','home','image','inbox','instagram','keyboard','keynote','layout','links','list','lock','login','logout','mac','mail','map','menu','message','mic','moon','music','notification','paragraph','pause','phone','picture-in-picture','play','plug','price-tag','print','qr-code','question','reddit','refresh','restart','rocket','rss','scales','search','secure-payment','send','settings','share','shield','shopping-bag','slack','smartphone','sound-module','star','sun','t-box','tablet','tag','telegram','thumb-up','timer','tool','trophy','twitter','tv','upload','usb','user','video','visa','voicemail','volume-up','wallet','wifi','windows','xbox','youtube','zoom-in'],
|
||||
bootstrap: ['alarm','android','apple','archive','arrow-down','arrow-up','arrow-left','arrow-right','at','award','backspace','badge-4k','bag','bank','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','brush','bug','calendar','camera','card-image','card-list','cart','chat','check','chevron-down','circle','cloud','code','command','compass','cpu','credit-card','database','device-hdd','device-ssd','display','download','droplet','earbuds','emoji-smile','envelope','exclamation','eye','facebook','file','filter','flag','folder','funnel','gear','gift','globe','google','graph-up','grid','hammer','hand-thumbs-up','hash','headphones','heart','house','image','info','instagram','joystick','keyboard','laptop','layers','layout-split','lightning','link','lock','mailbox','map','megaphone','menu-button','mic','moon','music-note','nut','palette','paperclip','patch-check','pen','pencil','people','phone','pin','play','plug','plus','power','printer','qr-code','question','rocket','rss','save','scissors','search','server','share','shield','shop','skip-forward','slack','speaker','speedometer','star','sun','tablet','tag','terminal','tools','trash','trophy','truck','twitch','twitter','type','ui-checks','upload','usb','vector-pen','wallet','whatsapp','wifi','windows','wrench','x','youtube'],
|
||||
heroicons: ['academic-cap','adjustments-horizontal','adjustments-vertical','archive-box','arrow-down','arrow-up','arrow-right','arrow-left','at-symbol','backspace','banknotes','bars-2','bars-3','battery-100','beaker','bell','bookmark','briefcase','cake','calendar','camera','chart-bar','chat-bubble-bottom-center','chat-bubble-left','check','chevron-down','chip','circle-stack','cloud','code-bracket','cog','command-line','computer-desktop','cpu-chip','cube','currency-dollar','device-phone-mobile','device-tablet','document','document-text','ellipsis-horizontal','envelope','exclamation-circle','eye','film','finger-print','fire','flag','folder','gift','globe-alt','hand-thumb-up','heart','home','identification','inbox','information-circle','key','language','lifebuoy','light-bulb','link','lock-closed','magnifying-glass','map','megaphone','microphone','moon','musical-note','newspaper','paint-brush','paper-airplane','paper-clip','phone','photo','play','plus','power','printer','puzzle-piece','qr-code','question-mark-circle','rocket-launch','rss','scale','scissors','server','share','shield-check','sparkles','square-3-stack-3d','star','sun','swatch','tag','trophy','tv','user','users','video-camera','wallet','wifi','wrench','x-mark'],
|
||||
feather: ['activity','airplay','alert-circle','alert-triangle','anchor','aperture','archive','at-sign','award','bar-chart','battery','bell','bluetooth','book','bookmark','box','briefcase','calendar','camera','cast','check','chevron-down','chrome','circle','clipboard','cloud','code','command','compass','cpu','database','download','droplet','edit','eye','facebook','file','film','filter','flag','folder','gift','git-branch','git-commit','git-merge','github','gitlab','globe','grid','hash','headphones','heart','help-circle','home','image','info','instagram','key','layers','layout','link','lock','mail','map','menu','mic','monitor','moon','music','package','paperclip','pause','pen-tool','phone','play','plus','pocket','power','printer','radio','refresh-ccw','refresh-cw','repeat','rewind','rss','save','scissors','search','send','server','settings','share','shield','shopping-bag','shopping-cart','shuffle','slack','smartphone','speaker','square','star','sun','tablet','tag','target','terminal','thumbs-up','tool','trash','trello','trending-up','triangle','truck','tv','twitter','type','umbrella','unlock','upload','user','users','video','voicemail','volume','watch','wifi','wind','x','zap'],
|
||||
simple: ['github','gitlab','google','youtube','twitter','facebook','twitch','discord','spotify','apple','microsoft','android','linux','ubuntu','x','linkedin','npm','pypi','docker','kubernetes','aws','azure','gcp','cloudflare','figma','notion','slack','whatsapp','meta','paypal','stripe','reddit','snapchat','steam','xbox','playstation','nintendo','instagram','pinterest','soundcloud','openai','vercel','netlify','digitalocean'],
|
||||
radix: ['activity-log','airplane','backpack','bell','bookmark','calendar','camera','card-stack','caret-down','caret-up','chat-bubble','chat-dots','check','chevron-down','chevron-left','chevron-right','chevron-up','clock','code','component-1','component-2','cookie','copy','cube','discord-logo','double-arrow-down','double-arrow-left','double-arrow-right','double-arrow-up','drag-handle-dots-2','envelope-closed','envelope-open','exclamation-triangle','external-link','eye-open','file','file-text','file-plus','gear','globe','heart','home','image','info-circled','keyboard','laptop','layers','link-1','link-2','lock-closed','magic-wand','magnifying-glass','moon','notebook','open-in-new-window','paper-plane','pencil-1','person','pie-chart','pin-left','pin-right','plus','question-mark-circled','reload','rocket','rows','scissors','share-1','share-2','shield','speaker-loud','star','sun','target','trash','upload','video','zoom-in','zoom-out']
|
||||
};
|
||||
|
||||
export const iconSets = {
|
||||
material: {
|
||||
label: 'Material',
|
||||
loader: async () => { try { return await fetchMaterialIcons(); } catch { return materialFallback; } },
|
||||
fetchIcon: async () => null
|
||||
},
|
||||
lucide: {
|
||||
label: 'Lucide',
|
||||
loader: async () => {
|
||||
const data = await attemptJSON('https://cdn.jsdelivr.net/npm/lucide@latest/dist/metadata.json', d => Object.keys(d));
|
||||
return data && data.length ? data : staticFallbacks.lucide;
|
||||
},
|
||||
fetchIcon: async (name) => {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${name}.svg`);
|
||||
return svg ? svgToDataUrl(svg) : null;
|
||||
}
|
||||
},
|
||||
tabler: {
|
||||
label: 'Tabler',
|
||||
loader: async () => {
|
||||
const data = await attemptJSON('https://cdn.jsdelivr.net/gh/tabler/tabler-icons@latest/icons.json', d => d.map(o => o.name));
|
||||
return data && data.length ? data : staticFallbacks.tabler;
|
||||
},
|
||||
fetchIcon: async (name) => {
|
||||
const urls = [
|
||||
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/outline/${name}.svg`,
|
||||
`https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/filled/${name}.svg`
|
||||
];
|
||||
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
|
||||
return null;
|
||||
}
|
||||
},
|
||||
phosphor: {
|
||||
label: 'Phosphor',
|
||||
loader: async () => staticFallbacks.phosphor,
|
||||
fetchIcon: async (name) => {
|
||||
const styles = ['regular','bold','duotone','fill','light','thin'];
|
||||
for (const style of styles) {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2/assets/${style}/${name}.svg`);
|
||||
if (svg) return svgToDataUrl(svg);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
remix: {
|
||||
label: 'Remix',
|
||||
loader: async () => staticFallbacks.remix,
|
||||
fetchIcon: async () => null,
|
||||
fontClass: (name) => `ri-${name}-line` // use line style font sprite
|
||||
},
|
||||
bootstrap: {
|
||||
label: 'Bootstrap',
|
||||
loader: async () => staticFallbacks.bootstrap,
|
||||
fetchIcon: async (name) => {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/icons/${name}.svg`);
|
||||
return svg ? svgToDataUrl(svg) : null;
|
||||
},
|
||||
fontClass: (name) => `bi-${name}`
|
||||
},
|
||||
heroicons: {
|
||||
label: 'Heroicons',
|
||||
loader: async () => staticFallbacks.heroicons,
|
||||
fetchIcon: async (name) => {
|
||||
const urls = [
|
||||
`https://cdn.jsdelivr.net/npm/heroicons@2/24/outline/${name}.svg`,
|
||||
`https://cdn.jsdelivr.net/npm/heroicons@2/24/solid/${name}.svg`
|
||||
];
|
||||
for (const u of urls) { const svg = await attemptText(u); if (svg) return svgToDataUrl(svg); }
|
||||
return null;
|
||||
}
|
||||
},
|
||||
feather: {
|
||||
label: 'Feather',
|
||||
loader: async () => staticFallbacks.feather,
|
||||
fetchIcon: async (name) => {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/feather-icons@4/dist/icons/${name}.svg`);
|
||||
return svg ? svgToDataUrl(svg) : null;
|
||||
},
|
||||
fontClass: (name) => `icon-${name}` // fallback for display
|
||||
},
|
||||
simple: {
|
||||
label: 'Simple Icons',
|
||||
loader: async () => staticFallbacks.simple,
|
||||
fetchIcon: async (name) => {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/${name}.svg`);
|
||||
return svg ? svgToDataUrl(svg) : null;
|
||||
}
|
||||
},
|
||||
radix: {
|
||||
label: 'Radix',
|
||||
loader: async () => staticFallbacks.radix,
|
||||
fetchIcon: async (name) => {
|
||||
const svg = await attemptText(`https://cdn.jsdelivr.net/npm/@radix-ui/icons@latest/icons/${name}.svg`);
|
||||
return svg ? svgToDataUrl(svg) : null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Utility: get list of set keys + label for UI
|
||||
export function listIconSets() {
|
||||
return Object.entries(iconSets).map(([key, val]) => ({ key, label: val.label }));
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// This file is automatically generated from Google's Material Icons.
|
||||
/**
|
||||
* Fetches the full list of Material Icon names from Google Fonts.
|
||||
* Returns an array of strings like ["3d_rotation","access_alarm",…]
|
||||
*/
|
||||
export async function fetchAllIcons() {
|
||||
const res = await fetch("https://fonts.google.com/metadata/icons");
|
||||
let txt = await res.text();
|
||||
// strip the weird prefix )]}'\n
|
||||
txt = txt.replace(/^\)\]\}'\s*/, "");
|
||||
const json = JSON.parse(txt);
|
||||
return json.icons.map(icon => icon.name);
|
||||
}
|
||||
|
||||
// Fallback static array for immediate use (e.g. the "+" button and bookmark icons)
|
||||
export const icons = [
|
||||
'add',
|
||||
'bookmark',
|
||||
'star',
|
||||
// …add any other icons your components expect synchronously…
|
||||
];
|
||||
@@ -0,0 +1,49 @@
|
||||
const zoomPercentEl = document.getElementById('zoom-percent');
|
||||
|
||||
function setCssVar(name, value, fallback) {
|
||||
const val = value || fallback;
|
||||
if (val) document.documentElement.style.setProperty(name, val);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const colors = theme?.colors || theme || {};
|
||||
setCssVar('--bg', colors.bg, '#0b0d10');
|
||||
setCssVar('--dark-blue', colors.darkBlue, '#0b1c2b');
|
||||
setCssVar('--dark-purple', colors.darkPurple, '#1b1035');
|
||||
setCssVar('--primary', colors.primary, '#7b2eff');
|
||||
setCssVar('--accent', colors.accent, '#00c6ff');
|
||||
setCssVar('--text', colors.text, '#e0e0e0');
|
||||
setCssVar('--url-bar-bg', colors.urlBarBg, '#1c2030');
|
||||
setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
|
||||
}
|
||||
|
||||
async function refreshZoom() {
|
||||
if (!window.electronAPI?.invoke || !zoomPercentEl) return;
|
||||
try {
|
||||
const z = await window.electronAPI.invoke('get-zoom-factor');
|
||||
zoomPercentEl.textContent = `${Math.round(z * 100)}%`;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
window.electronAPI?.on?.('menu-popup-init', (payload) => {
|
||||
applyTheme(payload?.theme);
|
||||
refreshZoom();
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-cmd]');
|
||||
if (!btn) return;
|
||||
const cmd = btn.getAttribute('data-cmd');
|
||||
window.electronAPI?.send?.('menu-popup-command', { cmd });
|
||||
if (cmd === 'zoom-in' || cmd === 'zoom-out') {
|
||||
setTimeout(refreshZoom, 50);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' });
|
||||
}
|
||||
});
|
||||
|
||||
refreshZoom();
|
||||
@@ -0,0 +1,38 @@
|
||||
const SEARCH_URL = 'https://www.google.com/search?q=';
|
||||
|
||||
function toNavigationUrl(input) {
|
||||
const value = (input || '').trim();
|
||||
if (!value) return null;
|
||||
if (/^(https?:|file:|data:|blob:)/i.test(value)) return value;
|
||||
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||
return `${SEARCH_URL}${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
function rememberSearch(input) {
|
||||
if (!input || input.includes('.') || /^(https?:|file:)/i.test(input)) return;
|
||||
try {
|
||||
const current = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
||||
const next = [input, ...current.filter(item => item !== input)].slice(0, 100);
|
||||
localStorage.setItem('searchHistory', JSON.stringify(next));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function navigateTo(input) {
|
||||
const target = toNavigationUrl(input);
|
||||
if (!target) return;
|
||||
rememberSearch(input.trim());
|
||||
window.location.href = target;
|
||||
}
|
||||
|
||||
window.NebulaCEF = { navigateTo, toNavigationUrl };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const form = document.getElementById('start-form');
|
||||
const urlInput = document.getElementById('start-url');
|
||||
if (!form || !urlInput) return;
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
navigateTo(urlInput.value);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,901 @@
|
||||
// Prefer contextBridge-exposed API
|
||||
const ipc = (window.electronAPI && typeof window.electronAPI.invoke === 'function')
|
||||
? window.electronAPI
|
||||
: null;
|
||||
|
||||
let clearBtn = document.getElementById('clear-data-btn');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const statusText = document.getElementById('status-text');
|
||||
const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
|
||||
const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f'
|
||||
const HOME_SEARCH_Y_KEY = 'nebula-home-search-y'; // number (vh)
|
||||
const HOME_BOOKMARKS_Y_KEY = 'nebula-home-bookmarks-y'; // number (vh)
|
||||
const HOME_GLANCE_CORNER_KEY = 'nebula-home-glance-corner'; // 'br'|'bl'|'tr'|'tl'
|
||||
const DISPLAY_SCALE_KEY = 'nebula-display-scale'; // number (50-300)
|
||||
|
||||
function showStatus(message) {
|
||||
if (statusText && statusDiv) {
|
||||
statusText.textContent = message;
|
||||
statusDiv.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
statusDiv.classList.add('hidden');
|
||||
}, 2000);
|
||||
} else {
|
||||
console.log('[STATUS]', message);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message) {
|
||||
if (!statusText || !statusDiv) {
|
||||
console.log('[STATUS]', message);
|
||||
return;
|
||||
}
|
||||
statusText.textContent = message;
|
||||
statusDiv.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
statusDiv.classList.add('hidden');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function attachClearHandler(btn) {
|
||||
if (!btn) return;
|
||||
btn.onclick = async () => {
|
||||
if (statusDiv && statusText) {
|
||||
statusDiv.classList.remove('hidden');
|
||||
statusText.textContent = 'Clearing cookies, storage, cache, and history...';
|
||||
}
|
||||
|
||||
try {
|
||||
if (ipc) {
|
||||
const ok = await ipc.invoke('clear-browser-data');
|
||||
// Also clear localStorage site history in this context
|
||||
try { localStorage.removeItem('siteHistory'); } catch {}
|
||||
// Try to refresh lists if present
|
||||
try { if (typeof loadHistories === 'function') await loadHistories(); } catch {}
|
||||
showStatus(ok
|
||||
? 'All browser data cleared.'
|
||||
: 'Failed to clear browser data.');
|
||||
} else {
|
||||
localStorage.clear();
|
||||
showStatus('Local page data cleared.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing browser data:', error);
|
||||
showStatus('An error occurred while clearing data.');
|
||||
} finally {
|
||||
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
|
||||
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('theme-update', currentTheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Try attaching immediately, and again on DOMContentLoaded
|
||||
attachClearHandler(clearBtn);
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (!clearBtn) {
|
||||
clearBtn = document.getElementById('clear-data-btn');
|
||||
attachClearHandler(clearBtn);
|
||||
}
|
||||
|
||||
// Wire per-section clear buttons to main when possible
|
||||
const clearSiteBtn = document.getElementById('clear-site-history-btn');
|
||||
if (clearSiteBtn) {
|
||||
clearSiteBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
// Clear localStorage copy
|
||||
try { localStorage.removeItem('siteHistory'); } catch {}
|
||||
// Ask main to clear file-based history for consistency
|
||||
if (ipc) { await ipc.invoke('clear-site-history'); }
|
||||
showStatus('Site history cleared');
|
||||
try { if (typeof loadHistories === 'function') await loadHistories(); } catch {}
|
||||
} catch (e) {
|
||||
console.error('Clear site history error:', e);
|
||||
showStatus('Failed clearing site history');
|
||||
}
|
||||
});
|
||||
}
|
||||
const clearSearchBtn = document.getElementById('clear-search-history-btn');
|
||||
if (clearSearchBtn) {
|
||||
clearSearchBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
// Clear from localStorage in this context
|
||||
try { localStorage.removeItem('searchHistory'); } catch {}
|
||||
|
||||
if (ipc) { await ipc.invoke('clear-search-history'); }
|
||||
showStatus('Search history cleared');
|
||||
} catch (e) {
|
||||
console.error('Clear search history error:', e);
|
||||
showStatus('Failed clearing search history');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Weather unit controls
|
||||
try {
|
||||
const stored = localStorage.getItem(WEATHER_UNIT_KEY) || 'auto';
|
||||
const radios = document.querySelectorAll('input[name="weather-unit"]');
|
||||
radios.forEach(r => r.checked = (r.value === stored));
|
||||
radios.forEach(radio => radio.addEventListener('change', () => {
|
||||
const val = document.querySelector('input[name="weather-unit"]:checked')?.value || 'auto';
|
||||
localStorage.setItem(WEATHER_UNIT_KEY, val);
|
||||
showStatus(`Weather units set to ${val === 'c' ? 'Celsius' : val === 'f' ? 'Fahrenheit' : 'Auto'}`);
|
||||
// Hint home page to refresh weather if it listens to storage events
|
||||
try { window.dispatchEvent(new StorageEvent('storage', { key: WEATHER_UNIT_KEY, newValue: val })); } catch {}
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('settings-update', { weatherUnit: val });
|
||||
}
|
||||
}));
|
||||
} catch (e) { console.warn('Weather unit setup failed', e); }
|
||||
|
||||
// Home layout controls
|
||||
try {
|
||||
const searchRange = document.getElementById('home-search-y');
|
||||
const searchVal = document.getElementById('home-search-y-val');
|
||||
const bmRange = document.getElementById('home-bookmarks-y');
|
||||
const bmVal = document.getElementById('home-bookmarks-y-val');
|
||||
const cornerRadios = document.querySelectorAll('input[name="home-glance-corner"]');
|
||||
|
||||
const initNum = (key, def, input, label) => {
|
||||
const v = Number(localStorage.getItem(key) || def);
|
||||
if (input) input.value = String(v);
|
||||
if (label) label.textContent = v + 'vh';
|
||||
return v;
|
||||
};
|
||||
initNum(HOME_SEARCH_Y_KEY, 22, searchRange, searchVal);
|
||||
initNum(HOME_BOOKMARKS_Y_KEY, 40, bmRange, bmVal);
|
||||
const storedCorner = localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br';
|
||||
cornerRadios.forEach(r => r.checked = (r.value === storedCorner));
|
||||
|
||||
const notify = () => {
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('settings-update', {
|
||||
searchY: Number(localStorage.getItem(HOME_SEARCH_Y_KEY) || 22),
|
||||
bookmarksY: Number(localStorage.getItem(HOME_BOOKMARKS_Y_KEY) || 40),
|
||||
glanceCorner: localStorage.getItem(HOME_GLANCE_CORNER_KEY) || 'br'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (searchRange) searchRange.addEventListener('input', () => {
|
||||
const val = Number(searchRange.value);
|
||||
searchVal.textContent = val + 'vh';
|
||||
localStorage.setItem(HOME_SEARCH_Y_KEY, String(val));
|
||||
notify();
|
||||
});
|
||||
if (bmRange) bmRange.addEventListener('input', () => {
|
||||
const val = Number(bmRange.value);
|
||||
bmVal.textContent = val + 'vh';
|
||||
localStorage.setItem(HOME_BOOKMARKS_Y_KEY, String(val));
|
||||
notify();
|
||||
});
|
||||
cornerRadios.forEach(r => r.addEventListener('change', () => {
|
||||
const val = document.querySelector('input[name="home-glance-corner"]:checked')?.value || 'br';
|
||||
localStorage.setItem(HOME_GLANCE_CORNER_KEY, val);
|
||||
notify();
|
||||
}));
|
||||
} catch (e) { console.warn('Home layout control setup failed', e); }
|
||||
|
||||
// Display scale controls
|
||||
try {
|
||||
const scaleValue = document.getElementById('display-scale-value');
|
||||
const zoomDecrease = document.getElementById('zoom-decrease');
|
||||
const zoomIncrease = document.getElementById('zoom-increase');
|
||||
const zoomPresets = document.querySelectorAll('.zoom-preset-btn');
|
||||
|
||||
let currentScale = Number(localStorage.getItem(DISPLAY_SCALE_KEY) || 100);
|
||||
|
||||
// Function to apply zoom
|
||||
async function applyZoom(scale) {
|
||||
currentScale = Math.max(50, Math.min(300, scale));
|
||||
if (scaleValue) scaleValue.textContent = currentScale + '%';
|
||||
localStorage.setItem(DISPLAY_SCALE_KEY, String(currentScale));
|
||||
|
||||
// Highlight active preset
|
||||
zoomPresets.forEach(btn => {
|
||||
btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale);
|
||||
});
|
||||
|
||||
if (ipc && typeof ipc.invoke === 'function') {
|
||||
try {
|
||||
const zoomFactor = currentScale / 100;
|
||||
await ipc.invoke('set-zoom-factor', zoomFactor);
|
||||
showStatus(`Zoom set to ${currentScale}%`);
|
||||
} catch (err) {
|
||||
console.warn('Failed to apply zoom:', err);
|
||||
showStatus(`Zoom saved to ${currentScale}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize display
|
||||
if (scaleValue) scaleValue.textContent = currentScale + '%';
|
||||
zoomPresets.forEach(btn => {
|
||||
btn.classList.toggle('active', Number(btn.dataset.zoom) === currentScale);
|
||||
});
|
||||
|
||||
// Apply saved zoom on load
|
||||
if (ipc && typeof ipc.invoke === 'function' && currentScale !== 100) {
|
||||
try {
|
||||
const zoomFactor = currentScale / 100;
|
||||
ipc.invoke('set-zoom-factor', zoomFactor).catch(err => {
|
||||
console.warn('Failed to apply initial zoom:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Failed to apply initial zoom:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Decrease button
|
||||
if (zoomDecrease) {
|
||||
zoomDecrease.addEventListener('click', () => {
|
||||
applyZoom(currentScale - 10);
|
||||
});
|
||||
}
|
||||
|
||||
// Increase button
|
||||
if (zoomIncrease) {
|
||||
zoomIncrease.addEventListener('click', () => {
|
||||
applyZoom(currentScale + 10);
|
||||
});
|
||||
}
|
||||
|
||||
// Preset buttons
|
||||
zoomPresets.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const zoom = Number(btn.dataset.zoom);
|
||||
applyZoom(zoom);
|
||||
});
|
||||
});
|
||||
} catch (e) { console.warn('Display scale setup failed', e); }
|
||||
|
||||
// Big Picture Mode controls
|
||||
try {
|
||||
const bigPictureBtn = document.getElementById('launch-bigpicture-btn');
|
||||
const bigPictureStatus = document.getElementById('bigpicture-status');
|
||||
|
||||
// Check if Big Picture Mode is recommended for this display
|
||||
if (window.bigPictureAPI && typeof window.bigPictureAPI.isSuggested === 'function') {
|
||||
window.bigPictureAPI.isSuggested().then(suggested => {
|
||||
if (suggested && bigPictureStatus) {
|
||||
bigPictureStatus.textContent = '✓ Recommended for your display';
|
||||
bigPictureStatus.style.color = '#4ade80';
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
// Get screen info for display
|
||||
window.bigPictureAPI.getScreenInfo().then(info => {
|
||||
if (info && bigPictureStatus) {
|
||||
const hint = info.isSteamDeck ? 'Steam Deck detected' :
|
||||
info.isSmallScreen ? 'Small screen detected' : '';
|
||||
if (hint && !bigPictureStatus.textContent) {
|
||||
bigPictureStatus.textContent = hint;
|
||||
}
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
if (bigPictureBtn) {
|
||||
bigPictureBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
if (window.bigPictureAPI && typeof window.bigPictureAPI.launch === 'function') {
|
||||
showStatus('Launching Big Picture Mode...');
|
||||
await window.bigPictureAPI.launch();
|
||||
} else {
|
||||
showStatus('Big Picture Mode not available');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Big Picture Mode launch error:', e);
|
||||
showStatus('Failed to launch Big Picture Mode');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) { console.warn('Big Picture Mode setup failed', e); }
|
||||
});
|
||||
|
||||
// Tabs: simple controller
|
||||
function activateTab(tabName) {
|
||||
const links = document.querySelectorAll('.tab-link');
|
||||
const panels = document.querySelectorAll('.tab-panel');
|
||||
|
||||
links.forEach(l => {
|
||||
const isActive = l.dataset.tab === tabName;
|
||||
l.classList.toggle('active', isActive);
|
||||
l.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
if (isActive) l.focus({ preventScroll: true });
|
||||
});
|
||||
panels.forEach(p => {
|
||||
const isActive = p.id === `panel-${tabName}`;
|
||||
p.classList.toggle('active', isActive);
|
||||
p.hidden = !isActive;
|
||||
// noop
|
||||
});
|
||||
try { localStorage.setItem(TAB_STORAGE_KEY, tabName); } catch {}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
const links = document.querySelectorAll('.tab-link');
|
||||
|
||||
const getFocusableElements = (container) => {
|
||||
if (!container) return [];
|
||||
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
return Array.from(container.querySelectorAll(selector))
|
||||
.filter(el => !el.disabled && el.getAttribute('aria-hidden') !== 'true' && el.offsetParent !== null);
|
||||
};
|
||||
|
||||
const focusFirstInActivePanel = () => {
|
||||
const activePanel = document.querySelector('.tab-panel.active') || null;
|
||||
const focusables = getFocusableElements(activePanel);
|
||||
if (focusables.length > 0) {
|
||||
focusables[0].focus({ preventScroll: true });
|
||||
return true;
|
||||
}
|
||||
if (activePanel) {
|
||||
if (!activePanel.hasAttribute('tabindex')) {
|
||||
activePanel.setAttribute('tabindex', '-1');
|
||||
}
|
||||
activePanel.focus({ preventScroll: true });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Direct listeners (for accessibility focus handling)
|
||||
links.forEach((link, index) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const name = link.dataset.tab;
|
||||
if (!name) return;
|
||||
if (location.hash !== `#${name}`) {
|
||||
history.replaceState(null, '', `#${name}`);
|
||||
}
|
||||
activateTab(name);
|
||||
});
|
||||
|
||||
// Controller/keyboard: move from tab to panel content
|
||||
link.addEventListener('keydown', (e) => {
|
||||
if (e.defaultPrevented) return;
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
||||
const moved = focusFirstInActivePanel();
|
||||
if (moved) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delegation as a fallback if elements are re-rendered
|
||||
const tabContainer = document.querySelector('.tabs');
|
||||
if (tabContainer) {
|
||||
tabContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target && e.target.closest ? e.target.closest('.tab-link') : null;
|
||||
if (!btn || !tabContainer.contains(btn)) return;
|
||||
const name = btn.dataset.tab;
|
||||
if (!name) return;
|
||||
if (location.hash !== `#${name}`) {
|
||||
history.replaceState(null, '', `#${name}`);
|
||||
}
|
||||
activateTab(name);
|
||||
});
|
||||
}
|
||||
|
||||
// Global fallback: if focus is on sidebar tabs, move into active panel on down/right
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.defaultPrevented) return;
|
||||
if (e.key !== 'ArrowDown' && e.key !== 'ArrowRight') return;
|
||||
|
||||
const activeEl = document.activeElement;
|
||||
const inTabs = activeEl && (activeEl.classList?.contains('tab-link') || activeEl.closest?.('.tabs'));
|
||||
const inSidebar = activeEl && activeEl.closest?.('.sidebar');
|
||||
|
||||
if (inTabs || inSidebar) {
|
||||
const moved = focusFirstInActivePanel();
|
||||
if (moved) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Resolve initial tab: hash > storage > default 'general'
|
||||
let initial = (location.hash || '').replace('#', '') || null;
|
||||
if (!initial) {
|
||||
try { initial = localStorage.getItem(TAB_STORAGE_KEY) || null; } catch {}
|
||||
}
|
||||
if (!initial) initial = 'general';
|
||||
activateTab(initial);
|
||||
}
|
||||
|
||||
// Initialize tabs after DOM is ready but before customization init uses the DOM
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initTabs();
|
||||
});
|
||||
|
||||
// Apply current theme to settings page
|
||||
function applyCurrentThemeToSettings() {
|
||||
if (!window.BrowserCustomizer) return;
|
||||
|
||||
const savedTheme = localStorage.getItem('nebula-theme');
|
||||
let theme = null;
|
||||
|
||||
if (savedTheme) {
|
||||
try {
|
||||
theme = JSON.parse(savedTheme);
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse saved theme', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!theme || !theme.colors) return;
|
||||
|
||||
// Apply theme colors to CSS variables
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--bg', theme.colors.bg || '#121418');
|
||||
root.style.setProperty('--gradient-end', theme.colors.darkPurple || '#1B1035');
|
||||
root.style.setProperty('--primary', theme.colors.primary || '#7B2EFF');
|
||||
root.style.setProperty('--accent', theme.colors.accent || '#00C6FF');
|
||||
root.style.setProperty('--text', theme.colors.text || '#E0E0E0');
|
||||
|
||||
// Update glow colors based on theme
|
||||
const primaryRgb = hexToRgb(theme.colors.primary || '#7B2EFF');
|
||||
if (primaryRgb) {
|
||||
root.style.setProperty('--ring', `0 0 0 2px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.4)`);
|
||||
root.style.setProperty('--glow-subtle', `0 4px 20px rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.15)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert hex to RGB
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
// Listen for theme changes
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'nebula-theme') {
|
||||
applyCurrentThemeToSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// About tab population
|
||||
async function populateAbout() {
|
||||
try {
|
||||
const info = (window.aboutAPI && typeof window.aboutAPI.getInfo === 'function')
|
||||
? await window.aboutAPI.getInfo()
|
||||
: null;
|
||||
const byId = (id) => document.getElementById(id);
|
||||
if (!info || info.error) {
|
||||
byId('about-app-name').textContent = 'Nebula Browser';
|
||||
byId('about-app-version').textContent = 'CEF build';
|
||||
const versionCopy = byId('about-app-version-copy');
|
||||
if (versionCopy) versionCopy.textContent = 'CEF build';
|
||||
byId('about-packaged').textContent = 'Native';
|
||||
byId('about-userdata').textContent = 'Managed by CEF';
|
||||
byId('about-cef').textContent = navigator.userAgent;
|
||||
byId('about-chrome').textContent = navigator.userAgent.match(/Chrome\/([^\s]+)/)?.[1] || 'Unknown';
|
||||
byId('about-node').textContent = 'Not available';
|
||||
byId('about-v8').textContent = 'Managed by Chromium';
|
||||
byId('about-os').textContent = navigator.platform || 'Unknown';
|
||||
byId('about-cpu').textContent = navigator.hardwareConcurrency ? `${navigator.hardwareConcurrency} logical cores` : 'Unknown';
|
||||
byId('about-arch').textContent = navigator.userAgentData?.platform || navigator.platform || 'Unknown';
|
||||
byId('about-mem').textContent = navigator.deviceMemory ? `${navigator.deviceMemory} GB estimate` : 'Unknown';
|
||||
return;
|
||||
}
|
||||
byId('about-app-name').textContent = info.appName;
|
||||
byId('about-app-version').textContent = info.appVersion;
|
||||
byId('about-packaged').textContent = info.isPackaged ? 'Yes' : 'No';
|
||||
byId('about-userdata').textContent = info.userDataPath;
|
||||
|
||||
byId('about-cef').textContent = info.cefVersion || info.chromeVersion || 'Chromium Embedded Framework';
|
||||
byId('about-chrome').textContent = info.chromeVersion;
|
||||
byId('about-node').textContent = info.nodeVersion;
|
||||
byId('about-v8').textContent = info.v8Version;
|
||||
|
||||
byId('about-os').textContent = `${info.osType} ${info.osRelease}`;
|
||||
byId('about-cpu').textContent = info.cpu;
|
||||
byId('about-arch').textContent = info.arch;
|
||||
byId('about-mem').textContent = `${info.totalMemGB} GB`;
|
||||
|
||||
const copyBtn = document.getElementById('copy-about-btn');
|
||||
if (copyBtn && !copyBtn.dataset.listenerAttached) {
|
||||
copyBtn.dataset.listenerAttached = 'true';
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
const payload = [
|
||||
`Nebula ${info.appVersion} (${info.isPackaged ? 'packaged' : 'dev'})`,
|
||||
`CEF ${info.cefVersion || info.chromeVersion || 'unknown'} | Chromium ${info.chromeVersion} | V8 ${info.v8Version}`,
|
||||
`${info.osType} ${info.osRelease} ${info.arch}`,
|
||||
`CPU: ${info.cpu}`,
|
||||
`RAM: ${info.totalMemGB} GB`,
|
||||
`UserData: ${info.userDataPath}`
|
||||
].join('\n');
|
||||
try {
|
||||
await navigator.clipboard.writeText(payload);
|
||||
showStatus('Diagnostics copied');
|
||||
} catch (err) {
|
||||
console.error('Clipboard error:', err);
|
||||
showStatus('Failed to copy diagnostics');
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ABOUT] Error populating about info:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate about info after DOM is ready
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
populateAbout();
|
||||
applyCurrentThemeToSettings();
|
||||
|
||||
// Refresh about info when About tab is clicked
|
||||
const aboutTabBtn = document.getElementById('tab-about');
|
||||
if (aboutTabBtn) {
|
||||
aboutTabBtn.addEventListener('click', () => {
|
||||
// Refresh after a short delay to allow tab transition
|
||||
setTimeout(() => {
|
||||
populateAbout();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Electron updater feature setup (for security updates)
|
||||
async function setupElectronUpdater() {
|
||||
const securityUpdatesSection = document.querySelector('.customization-group:has(#electron-update-banner)');
|
||||
const banner = document.getElementById('electron-update-banner');
|
||||
const statusSpan = document.getElementById('electron-update-status');
|
||||
const detailsDiv = document.getElementById('electron-update-details');
|
||||
const progressDiv = document.getElementById('electron-update-progress');
|
||||
const checkBtn = document.getElementById('check-electron-versions');
|
||||
const upgradeBtn = document.getElementById('electron-upgrade-btn');
|
||||
const versionSelect = document.getElementById('electron-version-select');
|
||||
const currentVersionSpan = document.getElementById('electron-current-version');
|
||||
const appVersionSpan = document.getElementById('about-app-version-copy');
|
||||
|
||||
if (!ipc) {
|
||||
console.warn('[ELECTRON-UPDATER] IPC not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if app is packaged - if so, hide the entire Security Updates section
|
||||
try {
|
||||
const appInfo = await ipc.invoke('get-app-info');
|
||||
console.log('[ELECTRON-UPDATER] App info:', appInfo);
|
||||
|
||||
if (appInfo && appInfo.isPackaged) {
|
||||
console.log('[ELECTRON-UPDATER] Packaged build detected - hiding Security Updates section');
|
||||
if (securityUpdatesSection) {
|
||||
securityUpdatesSection.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ELECTRON-UPDATER] Development mode - showing Security Updates section');
|
||||
} catch (err) {
|
||||
console.error('[ELECTRON-UPDATER] Failed to get app info:', err);
|
||||
// On error, hide the section to be safe
|
||||
if (securityUpdatesSection) {
|
||||
securityUpdatesSection.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let availableVersion = null;
|
||||
let currentVersion = null;
|
||||
let isUpgrading = false;
|
||||
|
||||
// Get current app version
|
||||
try {
|
||||
const info = await window.aboutAPI?.getInfo();
|
||||
if (info && appVersionSpan) {
|
||||
appVersionSpan.textContent = info.appVersion || 'Unknown';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ELECTRON-UPDATER] Failed to get app version:', err);
|
||||
}
|
||||
|
||||
// Check for Electron updates
|
||||
const checkVersions = async () => {
|
||||
if (isUpgrading) return;
|
||||
|
||||
try {
|
||||
checkBtn.disabled = true;
|
||||
banner.style.display = 'block';
|
||||
statusSpan.textContent = 'Checking for updates...';
|
||||
detailsDiv.textContent = '';
|
||||
progressDiv.style.display = 'none';
|
||||
upgradeBtn.style.display = 'none';
|
||||
banner.style.borderColor = 'rgba(123, 46, 255, 0.3)';
|
||||
banner.style.background = 'rgba(123, 46, 255, 0.1)';
|
||||
|
||||
const buildType = versionSelect.value;
|
||||
const result = await ipc.invoke('get-electron-versions', buildType);
|
||||
|
||||
if (result.error) {
|
||||
statusSpan.textContent = 'Update check failed';
|
||||
detailsDiv.textContent = result.error;
|
||||
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
|
||||
banner.style.background = 'rgba(244, 67, 54, 0.1)';
|
||||
showStatus(`Failed: ${result.error}`);
|
||||
} else {
|
||||
availableVersion = result.available;
|
||||
currentVersion = result.current;
|
||||
|
||||
if (currentVersionSpan) {
|
||||
currentVersionSpan.textContent = currentVersion || 'Unknown';
|
||||
}
|
||||
|
||||
const isNewer = compareVersions(availableVersion, currentVersion) > 0;
|
||||
|
||||
if (isNewer) {
|
||||
statusSpan.textContent = 'Security update available';
|
||||
detailsDiv.textContent = `Electron ${availableVersion} is available (you have ${currentVersion}). This update includes security patches and performance improvements.`;
|
||||
upgradeBtn.style.display = 'inline-block';
|
||||
upgradeBtn.disabled = false;
|
||||
banner.style.borderColor = 'rgba(76, 175, 80, 0.5)';
|
||||
banner.style.background = 'rgba(76, 175, 80, 0.1)';
|
||||
showStatus(`Update available: ${availableVersion}`);
|
||||
} else {
|
||||
statusSpan.textContent = 'Up to date';
|
||||
detailsDiv.textContent = `You are running the latest ${buildType} version of Electron (${currentVersion}).`;
|
||||
upgradeBtn.style.display = 'none';
|
||||
banner.style.borderColor = 'rgba(100, 100, 100, 0.3)';
|
||||
banner.style.background = 'rgba(100, 100, 100, 0.1)';
|
||||
showStatus('Electron is up to date');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ELECTRON-UPDATER] Check failed:', err);
|
||||
statusSpan.textContent = 'Update check failed';
|
||||
detailsDiv.textContent = err.message;
|
||||
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
|
||||
banner.style.background = 'rgba(244, 67, 54, 0.1)';
|
||||
showStatus('Check failed');
|
||||
} finally {
|
||||
checkBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Install Electron update
|
||||
const handleUpgrade = async () => {
|
||||
if (isUpgrading) return;
|
||||
|
||||
const buildType = versionSelect.value;
|
||||
if (!availableVersion) {
|
||||
showStatus('No update available');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(
|
||||
`Update Electron from ${currentVersion} to ${availableVersion}?\n\nThis will download and install the ${buildType} version, then restart the application.\n\nThis process may take a few minutes.`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
isUpgrading = true;
|
||||
upgradeBtn.disabled = true;
|
||||
checkBtn.disabled = true;
|
||||
versionSelect.disabled = true;
|
||||
|
||||
statusSpan.textContent = 'Installing update...';
|
||||
detailsDiv.textContent = `Downloading and installing Electron ${availableVersion}. Please wait...`;
|
||||
progressDiv.style.display = 'block';
|
||||
banner.style.borderColor = 'rgba(255, 193, 7, 0.5)';
|
||||
banner.style.background = 'rgba(255, 193, 7, 0.1)';
|
||||
showStatus('Installing Electron update...');
|
||||
|
||||
const result = await ipc.invoke('upgrade-electron', buildType);
|
||||
|
||||
if (result.success) {
|
||||
statusSpan.textContent = 'Update installed';
|
||||
detailsDiv.textContent = 'Electron has been updated successfully. The application will restart now.';
|
||||
progressDiv.style.display = 'none';
|
||||
banner.style.borderColor = 'rgba(76, 175, 80, 0.5)';
|
||||
banner.style.background = 'rgba(76, 175, 80, 0.1)';
|
||||
showStatus('Update complete - restarting...');
|
||||
|
||||
// Restart the app
|
||||
setTimeout(() => {
|
||||
if (ipc) {
|
||||
ipc.invoke('restart-app').catch(err => {
|
||||
console.error('Restart failed:', err);
|
||||
showStatus('Please restart the app manually');
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error(result.error || 'Upgrade failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ELECTRON-UPDATER] Upgrade failed:', err);
|
||||
statusSpan.textContent = 'Update failed';
|
||||
detailsDiv.textContent = `Failed to install update: ${err.message}`;
|
||||
progressDiv.style.display = 'none';
|
||||
banner.style.borderColor = 'rgba(244, 67, 54, 0.5)';
|
||||
banner.style.background = 'rgba(244, 67, 54, 0.1)';
|
||||
showStatus(`Update failed: ${err.message}`);
|
||||
|
||||
isUpgrading = false;
|
||||
upgradeBtn.disabled = false;
|
||||
checkBtn.disabled = false;
|
||||
versionSelect.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Wire up event handlers
|
||||
if (checkBtn) {
|
||||
checkBtn.addEventListener('click', checkVersions);
|
||||
}
|
||||
|
||||
if (upgradeBtn) {
|
||||
upgradeBtn.addEventListener('click', handleUpgrade);
|
||||
}
|
||||
|
||||
if (versionSelect) {
|
||||
versionSelect.addEventListener('change', () => {
|
||||
// Reset UI when build type changes
|
||||
banner.style.display = 'none';
|
||||
upgradeBtn.style.display = 'none';
|
||||
upgradeBtn.disabled = true;
|
||||
availableVersion = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function to compare semantic versions
|
||||
function compareVersions(v1, v2) {
|
||||
const parts1 = v1.split('-')[0].split('.').map(x => parseInt(x, 10));
|
||||
const parts2 = v2.split('-')[0].split('.').map(x => parseInt(x, 10));
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
if (p1 > p2) return 1;
|
||||
if (p1 < p2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Keep settings open when clicking GitHub by asking host to open externally/new tab
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const gh = document.getElementById('github-link');
|
||||
if (gh) {
|
||||
gh.addEventListener('click', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
const url = gh.getAttribute('href');
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('navigate', url, { newTab: true });
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*');
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to open GitHub link:', err);
|
||||
window.open(gh.getAttribute('href'), '_blank');
|
||||
}
|
||||
});
|
||||
}
|
||||
const help = document.getElementById('help-link');
|
||||
if (help) {
|
||||
help.addEventListener('click', (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
const url = help.getAttribute('href');
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
window.electronAPI.sendToHost('navigate', url, { newTab: true });
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'navigate', url, newTab: true }, '*');
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to open Help link:', err);
|
||||
window.open(help.getAttribute('href'), '_blank');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Plugins management (Settings)
|
||||
// -----------------------------
|
||||
async function loadPluginsUI() {
|
||||
const listEl = document.getElementById('plugins-list');
|
||||
const reloadAllBtn = document.getElementById('plugins-reload-all');
|
||||
if (!listEl) return;
|
||||
// Load list
|
||||
let items = [];
|
||||
try {
|
||||
items = (ipc ? await ipc.invoke('plugins-list') : []) || [];
|
||||
} catch (e) {
|
||||
console.warn('plugins-list failed', e);
|
||||
}
|
||||
listEl.innerHTML = '';
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'plugin-item';
|
||||
empty.textContent = 'No plugins found';
|
||||
listEl.appendChild(empty);
|
||||
} else {
|
||||
for (const p of items) {
|
||||
const categories = Array.isArray(p.categories) ? p.categories.filter(x => x && typeof x === 'string') : [];
|
||||
const authors = Array.isArray(p.authors) ? p.authors.filter(x => x && typeof x === 'string') : [];
|
||||
const tagsHtml = categories.length ? `<div class="plugin-tags">${categories.map(c => `<span class=\"plugin-tag\">${escapeHtml(c)}</span>`).join('')}</div>` : '';
|
||||
const authorsHtml = authors.length ? `<div class=\"plugin-authors\"><span class=\"muted\">Authors:</span> ${authors.map(a => `<span class=\"plugin-author\">${escapeHtml(a)}</span>`).join(', ')}</div>` : '';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'plugin-item';
|
||||
row.setAttribute('role', 'listitem');
|
||||
row.innerHTML = `
|
||||
<div class="plugin-meta">
|
||||
<div class="plugin-title">${escapeHtml(p.name)} <span style="opacity:.7;font-weight:400">v${escapeHtml(p.version)}</span></div>
|
||||
<div class="plugin-desc">${escapeHtml(p.description || '')}</div>
|
||||
${tagsHtml}
|
||||
${authorsHtml}
|
||||
<div class="plugin-desc" style="opacity:.6; font-size:.85em;">${escapeHtml(p.dir)}</div>
|
||||
</div>
|
||||
<div class="plugin-actions">
|
||||
<label style="display:flex; align-items:center; gap:6px;">
|
||||
<input type="checkbox" class="plugin-enable" ${p.enabled ? 'checked' : ''}>
|
||||
<span>${p.enabled ? 'Enabled' : 'Disabled'}</span>
|
||||
</label>
|
||||
<span class="spacer"></span>
|
||||
<button class="plugin-reload">Reload</button>
|
||||
</div>`;
|
||||
// Wire actions
|
||||
const enableInput = row.querySelector('input.plugin-enable');
|
||||
const labelSpan = row.querySelector('label span');
|
||||
enableInput.addEventListener('change', async () => {
|
||||
const enabled = enableInput.checked;
|
||||
try {
|
||||
if (ipc) await ipc.invoke('plugins-set-enabled', { id: p.id, enabled });
|
||||
labelSpan.textContent = enabled ? 'Enabled' : 'Disabled';
|
||||
showStatus(`${p.name}: ${enabled ? 'Enabled' : 'Disabled'}.`);
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle plugin', p.id, e);
|
||||
enableInput.checked = !enabled;
|
||||
labelSpan.textContent = enableInput.checked ? 'Enabled' : 'Disabled';
|
||||
showStatus('Failed updating plugin');
|
||||
}
|
||||
});
|
||||
const reloadBtn = row.querySelector('button.plugin-reload');
|
||||
reloadBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
if (ipc) await ipc.invoke('plugins-reload', { id: p.id });
|
||||
showStatus(`${p.name} reloaded.`);
|
||||
} catch (e) {
|
||||
console.error('Plugin reload failed', e);
|
||||
showStatus('Reload failed');
|
||||
}
|
||||
});
|
||||
listEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
if (reloadAllBtn) reloadAllBtn.onclick = async () => {
|
||||
try { if (ipc) await ipc.invoke('plugins-reload', {}); showStatus('Plugins reloaded.'); } catch { showStatus('Reload failed'); }
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c]));
|
||||
}
|
||||
|
||||
// Load when settings page shows Plugins tab for the first time
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const tabBtn = document.getElementById('tab-plugins');
|
||||
if (!tabBtn) return;
|
||||
let loaded = false;
|
||||
const ensureLoad = () => { if (!loaded) { loaded = true; loadPluginsUI(); } };
|
||||
tabBtn.addEventListener('click', ensureLoad);
|
||||
if (location.hash === '#plugins') ensureLoad();
|
||||
});
|
||||
@@ -0,0 +1,611 @@
|
||||
/**
|
||||
* First-Time Setup Script for Nebula Browser
|
||||
* Handles theme selection, default browser setup, and first-run completion
|
||||
*/
|
||||
|
||||
// State management
|
||||
const setupState = {
|
||||
currentStep: 1,
|
||||
selectedTheme: 'default',
|
||||
defaultBrowserSet: false,
|
||||
skipped: false,
|
||||
themes: []
|
||||
};
|
||||
|
||||
const nativeApi = window.api || {
|
||||
async getAllThemes() {
|
||||
return {
|
||||
default: {
|
||||
default: {
|
||||
name: 'Default',
|
||||
description: 'Classic Nebula theme',
|
||||
colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' }
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
async isDefaultBrowser() {
|
||||
return false;
|
||||
},
|
||||
async setAsDefaultBrowser() {
|
||||
return { success: false, error: 'Default browser setup is handled by the native CEF app.' };
|
||||
},
|
||||
async applyTheme(themeId) {
|
||||
localStorage.setItem('activeThemeName', themeId);
|
||||
},
|
||||
async completeFirstRun(data) {
|
||||
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize setup when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
console.log('[Setup] Initializing first-time setup...');
|
||||
|
||||
// Load available themes
|
||||
await loadThemes();
|
||||
|
||||
// Initialize button handlers
|
||||
initializeButtons();
|
||||
|
||||
// Check default browser status
|
||||
checkDefaultBrowserStatus();
|
||||
});
|
||||
|
||||
/**
|
||||
* Load available themes from main process
|
||||
*/
|
||||
async function loadThemes() {
|
||||
try {
|
||||
const themes = await nativeApi.getAllThemes();
|
||||
console.log('[Setup] Loaded themes:', themes);
|
||||
setupState.themes = themes;
|
||||
|
||||
// Render theme grid
|
||||
renderThemeGrid(themes);
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error loading themes:', error);
|
||||
// Fallback to a default theme
|
||||
setupState.themes = {
|
||||
default: {
|
||||
default: { name: 'Default', description: 'Classic Nebula theme', colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF' } }
|
||||
}
|
||||
};
|
||||
renderThemeGrid(setupState.themes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render theme selection grid
|
||||
*/
|
||||
function renderThemeGrid(themes) {
|
||||
const themeGrid = document.getElementById('theme-grid');
|
||||
if (!themeGrid) return;
|
||||
|
||||
themeGrid.innerHTML = '';
|
||||
|
||||
// Convert themes object to array
|
||||
let themeArray = [];
|
||||
|
||||
if (Array.isArray(themes)) {
|
||||
// Already an array
|
||||
themeArray = themes;
|
||||
} else if (themes.default) {
|
||||
// Has default property, extract themes from it
|
||||
themeArray = Object.entries(themes.default).map(([id, data]) => ({
|
||||
id,
|
||||
name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '),
|
||||
description: data.description || 'A beautiful color scheme',
|
||||
colors: data.colors || {}
|
||||
}));
|
||||
} else {
|
||||
// Direct object of themes
|
||||
themeArray = Object.entries(themes).map(([id, data]) => ({
|
||||
id,
|
||||
name: data.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, ' '),
|
||||
description: data.description || 'A beautiful color scheme',
|
||||
colors: data.colors || {}
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('[Setup] Rendering', themeArray.length, 'themes');
|
||||
|
||||
// If no themes found, add a default one
|
||||
if (themeArray.length === 0) {
|
||||
themeArray = [{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
description: 'Classic Nebula theme',
|
||||
colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' }
|
||||
}];
|
||||
}
|
||||
|
||||
themeArray.forEach(theme => {
|
||||
const themeCard = createThemeCard(theme);
|
||||
themeGrid.appendChild(themeCard);
|
||||
});
|
||||
|
||||
// Select default theme
|
||||
const defaultCard = themeGrid.querySelector('[data-theme-id="default"]');
|
||||
if (defaultCard) {
|
||||
defaultCard.classList.add('selected');
|
||||
const defaultTheme = getThemeById('default');
|
||||
if (defaultTheme) {
|
||||
applyThemeToSetupPage(defaultTheme, 'default');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a theme by id from loaded theme sets
|
||||
*/
|
||||
function getThemeById(themeId) {
|
||||
const themes = setupState.themes || {};
|
||||
if (themes.default && themes.default[themeId]) return themes.default[themeId];
|
||||
if (themes.user && themes.user[themeId]) return themes.user[themeId];
|
||||
if (themes.downloaded && themes.downloaded[themeId]) return themes.downloaded[themeId];
|
||||
return null;
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
if (!hex || typeof hex !== 'string') return null;
|
||||
let normalized = hex.trim().replace(/^#/, '');
|
||||
if (normalized.length === 3) {
|
||||
normalized = normalized.split('').map(char => char + char).join('');
|
||||
}
|
||||
if (!/^[a-fA-F\d]{6}$/.test(normalized)) return null;
|
||||
|
||||
const intValue = parseInt(normalized, 16);
|
||||
return {
|
||||
r: (intValue >> 16) & 255,
|
||||
g: (intValue >> 8) & 255,
|
||||
b: intValue & 255
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to the setup page UI and persist selection
|
||||
*/
|
||||
function applyThemeToSetupPage(theme, themeId = null) {
|
||||
if (!theme || !theme.colors) return;
|
||||
const colors = theme.colors;
|
||||
const root = document.documentElement;
|
||||
|
||||
const setVar = (cssVar, value, fallback) => {
|
||||
const val = value || fallback;
|
||||
if (val) root.style.setProperty(cssVar, val);
|
||||
};
|
||||
|
||||
setVar('--bg', colors.bg, '#121418');
|
||||
setVar('--dark-blue', colors.darkBlue, '#0B1C2B');
|
||||
setVar('--dark-purple', colors.darkPurple, '#1B1035');
|
||||
setVar('--primary', colors.primary, '#7B2EFF');
|
||||
setVar('--accent', colors.accent, '#00C6FF');
|
||||
setVar('--text', colors.text, '#E0E0E0');
|
||||
setVar('--success', colors.accent, '#4CAF50');
|
||||
setVar('--warning', colors.primary, '#FF9800');
|
||||
|
||||
const primaryRgb = hexToRgb(colors.primary || '#7B2EFF');
|
||||
const accentRgb = hexToRgb(colors.accent || '#00C6FF');
|
||||
const successRgb = hexToRgb(colors.accent || '#4CAF50');
|
||||
const warningRgb = hexToRgb(colors.primary || '#FF9800');
|
||||
if (primaryRgb) {
|
||||
setVar('--primary-rgb', `${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}`);
|
||||
}
|
||||
if (accentRgb) {
|
||||
setVar('--accent-rgb', `${accentRgb.r}, ${accentRgb.g}, ${accentRgb.b}`);
|
||||
}
|
||||
if (successRgb) {
|
||||
setVar('--success-rgb', `${successRgb.r}, ${successRgb.g}, ${successRgb.b}`);
|
||||
}
|
||||
if (warningRgb) {
|
||||
setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`);
|
||||
}
|
||||
|
||||
if (theme.gradient) {
|
||||
document.body.style.background = theme.gradient;
|
||||
} else if (colors.bg) {
|
||||
document.body.style.background = colors.bg;
|
||||
}
|
||||
|
||||
// Persist for main UI to pick up on first load
|
||||
try {
|
||||
localStorage.setItem('currentTheme', JSON.stringify(theme));
|
||||
if (themeId) localStorage.setItem('activeThemeName', themeId);
|
||||
} catch (err) {
|
||||
console.warn('[Setup] Failed to persist theme:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a theme card element
|
||||
*/
|
||||
function createThemeCard(theme) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'theme-card';
|
||||
card.dataset.themeId = theme.id;
|
||||
|
||||
// Create color preview
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'theme-preview';
|
||||
|
||||
const colors = theme.colors || {};
|
||||
|
||||
// Get color values, trying multiple property naming conventions
|
||||
const getColor = (keys, fallback) => {
|
||||
for (const key of keys) {
|
||||
if (colors[key]) return colors[key];
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const previewColors = [
|
||||
getColor(['bg', '--bg', 'background'], '#121418'),
|
||||
getColor(['primary', '--primary'], '#7B2EFF'),
|
||||
getColor(['accent', '--accent'], '#00C6FF'),
|
||||
getColor(['text', '--text'], '#E0E0E0')
|
||||
];
|
||||
|
||||
previewColors.forEach(color => {
|
||||
const colorDiv = document.createElement('div');
|
||||
colorDiv.className = 'theme-color';
|
||||
colorDiv.style.backgroundColor = color;
|
||||
preview.appendChild(colorDiv);
|
||||
});
|
||||
|
||||
// Create theme info
|
||||
const name = document.createElement('div');
|
||||
name.className = 'theme-name';
|
||||
name.textContent = theme.name || theme.id;
|
||||
|
||||
const description = document.createElement('div');
|
||||
description.className = 'theme-description';
|
||||
description.textContent = theme.description || 'A beautiful color scheme';
|
||||
|
||||
// Assemble card
|
||||
card.appendChild(preview);
|
||||
card.appendChild(name);
|
||||
card.appendChild(description);
|
||||
|
||||
// Add click handler
|
||||
card.addEventListener('click', () => selectTheme(theme.id, card));
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a theme
|
||||
*/
|
||||
function selectTheme(themeId, cardElement) {
|
||||
// Update state
|
||||
setupState.selectedTheme = themeId;
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.theme-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
cardElement.classList.add('selected');
|
||||
|
||||
const theme = getThemeById(themeId);
|
||||
if (theme) {
|
||||
applyThemeToSetupPage(theme, themeId);
|
||||
}
|
||||
|
||||
console.log('[Setup] Selected theme:', themeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Nebula is the default browser
|
||||
*/
|
||||
async function checkDefaultBrowserStatus() {
|
||||
const statusEl = document.getElementById('default-status');
|
||||
if (!statusEl) return;
|
||||
|
||||
statusEl.classList.add('checking');
|
||||
|
||||
try {
|
||||
const isDefault = await nativeApi.isDefaultBrowser();
|
||||
|
||||
statusEl.classList.remove('checking');
|
||||
|
||||
if (isDefault) {
|
||||
statusEl.classList.add('is-default');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">✓</div>
|
||||
<p class="status-text">Nebula is already your default browser</p>
|
||||
`;
|
||||
setupState.defaultBrowserSet = true;
|
||||
|
||||
// Update button
|
||||
const setDefaultBtn = document.getElementById('btn-set-default');
|
||||
if (setDefaultBtn) {
|
||||
setDefaultBtn.textContent = '✓ Already Default';
|
||||
setDefaultBtn.disabled = true;
|
||||
}
|
||||
} else {
|
||||
statusEl.classList.add('not-default');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">ℹ️</div>
|
||||
<p class="status-text">Nebula is not your default browser</p>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error checking default browser status:', error);
|
||||
statusEl.classList.remove('checking');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">⚠️</div>
|
||||
<p class="status-text">Unable to check default browser status</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Nebula as default browser
|
||||
*/
|
||||
async function setDefaultBrowser() {
|
||||
const btn = document.getElementById('btn-set-default');
|
||||
const statusEl = document.getElementById('default-status');
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="btn-icon">⏳</span> Setting...';
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await nativeApi.setAsDefaultBrowser();
|
||||
|
||||
if (result.success) {
|
||||
const isDefault = await window.api.isDefaultBrowser();
|
||||
if (isDefault) {
|
||||
setupState.defaultBrowserSet = true;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('not-default');
|
||||
statusEl.classList.add('is-default');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">✓</div>
|
||||
<p class="status-text">Nebula is now your default browser!</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.innerHTML = '<span class="btn-icon">✓</span> Set Successfully';
|
||||
}
|
||||
|
||||
// Auto-advance after a brief delay
|
||||
setTimeout(() => goToStep(4), 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.classList.remove('not-default');
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">ℹ️</div>
|
||||
<p class="status-text">System settings opened. Choose Nebula as your default browser to finish.</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="btn-icon">↻</span> Check Again';
|
||||
}
|
||||
|
||||
if (result.needsUserAction && nativeApi.openDefaultBrowserSettings) {
|
||||
try { await nativeApi.openDefaultBrowserSettings(); } catch {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(result.error || 'Failed to set default browser');
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error setting default browser:', error);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `
|
||||
<div class="status-icon">⚠️</div>
|
||||
<p class="status-text">Failed to set default browser. You can try again from settings.</p>
|
||||
`;
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span class="btn-icon">↻</span> Try Again';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific step
|
||||
*/
|
||||
function goToStep(stepNumber) {
|
||||
// Hide current step
|
||||
document.querySelectorAll('.setup-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show target step
|
||||
const targetStep = document.querySelector(`.setup-step[data-step="${stepNumber}"]`);
|
||||
if (targetStep) {
|
||||
targetStep.classList.add('active');
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
document.querySelectorAll('.progress-step').forEach((step, index) => {
|
||||
const stepNum = index + 1;
|
||||
if (stepNum < stepNumber) {
|
||||
step.classList.add('completed');
|
||||
step.classList.remove('active');
|
||||
} else if (stepNum === stepNumber) {
|
||||
step.classList.add('active');
|
||||
step.classList.remove('completed');
|
||||
} else {
|
||||
step.classList.remove('active', 'completed');
|
||||
}
|
||||
});
|
||||
|
||||
setupState.currentStep = stepNumber;
|
||||
|
||||
// Special handling for completion step
|
||||
if (stepNumber === 4) {
|
||||
renderCompletionSummary();
|
||||
}
|
||||
|
||||
console.log('[Setup] Navigated to step:', stepNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render completion summary
|
||||
*/
|
||||
function renderCompletionSummary() {
|
||||
const summaryEl = document.getElementById('completion-summary');
|
||||
if (!summaryEl) return;
|
||||
|
||||
const selectedThemeName = setupState.themes.default?.[setupState.selectedTheme]?.name ||
|
||||
setupState.selectedTheme.charAt(0).toUpperCase() + setupState.selectedTheme.slice(1);
|
||||
|
||||
summaryEl.innerHTML = `
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🎨</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">Selected Theme</div>
|
||||
<div class="summary-value">${selectedThemeName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-icon">🌐</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-label">Default Browser</div>
|
||||
<div class="summary-value">${setupState.defaultBrowserSet ? 'Set as Default' : 'Not Set'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete setup and save preferences
|
||||
*/
|
||||
async function completeSetup() {
|
||||
console.log('[Setup] Completing first-time setup...', setupState);
|
||||
|
||||
try {
|
||||
// Apply selected theme
|
||||
await nativeApi.applyTheme(setupState.selectedTheme);
|
||||
|
||||
// Save first-run completion
|
||||
await nativeApi.completeFirstRun({
|
||||
selectedTheme: setupState.selectedTheme,
|
||||
defaultBrowserSet: setupState.defaultBrowserSet,
|
||||
skipped: setupState.skipped
|
||||
});
|
||||
|
||||
console.log('[Setup] First-time setup completed successfully');
|
||||
|
||||
window.location.href = 'home.html';
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error completing setup:', error);
|
||||
alert('There was an error saving your preferences. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip setup and use defaults
|
||||
*/
|
||||
async function skipSetup() {
|
||||
setupState.skipped = true;
|
||||
|
||||
try {
|
||||
// Save that first-run was completed (even if skipped)
|
||||
await nativeApi.completeFirstRun({
|
||||
selectedTheme: 'default',
|
||||
defaultBrowserSet: false,
|
||||
skipped: true
|
||||
});
|
||||
|
||||
console.log('[Setup] Setup skipped, using defaults');
|
||||
|
||||
window.location.href = 'home.html';
|
||||
} catch (error) {
|
||||
console.error('[Setup] Error skipping setup:', error);
|
||||
window.location.href = 'home.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize button event handlers
|
||||
*/
|
||||
function initializeButtons() {
|
||||
// Step 1: Welcome
|
||||
const btnStart = document.getElementById('btn-start');
|
||||
const btnSkipAll = document.getElementById('btn-skip-all');
|
||||
|
||||
if (btnStart) {
|
||||
btnStart.addEventListener('click', () => goToStep(2));
|
||||
}
|
||||
|
||||
if (btnSkipAll) {
|
||||
btnSkipAll.addEventListener('click', skipSetup);
|
||||
}
|
||||
|
||||
// Step 2: Theme Selection
|
||||
const btnBack2 = document.getElementById('btn-back-2');
|
||||
const btnNext2 = document.getElementById('btn-next-2');
|
||||
|
||||
if (btnBack2) {
|
||||
btnBack2.addEventListener('click', () => goToStep(1));
|
||||
}
|
||||
|
||||
if (btnNext2) {
|
||||
btnNext2.addEventListener('click', () => goToStep(3));
|
||||
}
|
||||
|
||||
// Step 3: Default Browser
|
||||
const btnBack3 = document.getElementById('btn-back-3');
|
||||
const btnSkip3 = document.getElementById('btn-skip-3');
|
||||
const btnSetDefault = document.getElementById('btn-set-default');
|
||||
|
||||
if (btnBack3) {
|
||||
btnBack3.addEventListener('click', () => goToStep(2));
|
||||
}
|
||||
|
||||
if (btnSkip3) {
|
||||
btnSkip3.addEventListener('click', () => goToStep(4));
|
||||
}
|
||||
|
||||
if (btnSetDefault) {
|
||||
btnSetDefault.addEventListener('click', setDefaultBrowser);
|
||||
}
|
||||
|
||||
// Step 4: Complete
|
||||
const btnFinish = document.getElementById('btn-finish');
|
||||
|
||||
if (btnFinish) {
|
||||
btnFinish.addEventListener('click', completeSetup);
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const currentStep = setupState.currentStep;
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
goToStep(2);
|
||||
break;
|
||||
case 2:
|
||||
goToStep(3);
|
||||
break;
|
||||
case 3:
|
||||
if (!setupState.defaultBrowserSet) {
|
||||
setDefaultBrowser();
|
||||
} else {
|
||||
goToStep(4);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
completeSetup();
|
||||
break;
|
||||
}
|
||||
} else if (e.key === 'Escape' && setupState.currentStep > 1) {
|
||||
goToStep(setupState.currentStep - 1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>404 - Page Not Found</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
--bg:#121212; --panel:#1e1e1e; --warn:#d97706; --danger:#dc2626; --text:#f5f5f5; --muted:#9ca3af; --accent:#6366f1;
|
||||
color-scheme: dark;
|
||||
}
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Inter,Ubuntu,sans-serif; background:var(--bg); color:var(--text); display:flex; min-height:100vh; align-items:center; justify-content:center; padding:32px; }
|
||||
.card { max-width:780px; width:100%; background:linear-gradient(145deg,#1c1c1c,#242424); border:1px solid #2c2c2c; border-radius:20px; padding:40px 46px 48px; box-shadow:0 8px 28px -6px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,0.04); position:relative; overflow:hidden; }
|
||||
.card:before { content:""; position:absolute; inset:0; background:radial-gradient(circle at 18% 15%,rgba(255,255,255,.08),transparent 55%), radial-gradient(circle at 82% 78%,rgba(255,255,255,.05),transparent 60%); pointer-events:none; }
|
||||
h1 { font-size: clamp(1.9rem, 2.6vw, 2.6rem); margin:0 0 12px; letter-spacing:-.5px; display:flex; align-items:center; gap:.6rem; }
|
||||
h1 svg { width:28px; height:28px; flex-shrink:0; }
|
||||
h1 span.badge { font-size:12px; letter-spacing:1px; padding:4px 8px; border:1px solid var(--danger); color:var(--danger); border-radius:999px; text-transform:uppercase; background:rgba(220,38,38,0.1); }
|
||||
p.lede { font-size:1.05rem; line-height:1.55; margin:0 0 22px; color:var(--muted); }
|
||||
code { background:#252525; padding:3px 6px; border-radius:6px; font-size:.9rem; color:#e0e0e0; }
|
||||
.url-box { font-family:monospace; font-size:.92rem; padding:10px 12px; background:#181818; border:1px solid #2a2a2a; border-radius:10px; word-break:break-all; margin:0 0 22px; display:flex; align-items:center; gap:.75rem; }
|
||||
.url-box svg { flex:0 0 auto; width:22px; height:22px; stroke:var(--danger); }
|
||||
ul { margin:0 0 26px 1.1rem; padding:0; line-height:1.5; color:var(--muted); }
|
||||
ul li { margin-bottom:6px; }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:14px; }
|
||||
button { cursor:pointer; font-size:.95rem; letter-spacing:.4px; font-weight:500; border-radius:12px; padding:14px 26px; border:1px solid transparent; background:linear-gradient(135deg,#303030,#252525); color:#fff; position:relative; overflow:hidden; transition:.25s; }
|
||||
button.primary { background:linear-gradient(135deg,#6366f1,#5145cd); box-shadow:0 4px 18px -4px rgba(99,102,241,.5); }
|
||||
button.outline { background:transparent; border-color:#444; }
|
||||
button:hover { filter:brightness(1.12); transform:translateY(-2px); }
|
||||
button:active { transform:translateY(0); filter:brightness(.9); }
|
||||
.mini { font-size:.75rem; text-transform:uppercase; letter-spacing:1px; opacity:.8; margin-top:24px; }
|
||||
.fade-in { animation:fade .5s ease .05s both; }
|
||||
@keyframes fade { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform:none; } }
|
||||
.grid { display:grid; gap:40px; }
|
||||
@media (max-width:760px){ .card{padding:34px 28px 40px;} h1{font-size:2rem;} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card fade-in">
|
||||
<h1>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
|
||||
Page Not Found <span class="badge">404</span>
|
||||
</h1>
|
||||
<p class="lede">The page you're looking for doesn't exist or has been moved. You've warped into an unknown sector of the web.</p>
|
||||
<div class="url-box" id="targetBox" title="Attempted URL"></div>
|
||||
<ul>
|
||||
<li>The URL might be typed incorrectly.</li>
|
||||
<li>The page may have been removed or relocated.</li>
|
||||
<li>The link you followed could be outdated or broken.</li>
|
||||
<li>Try going back or navigate to the home page.</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<button id="backBtn" class="outline" aria-label="Go Back">Go Back</button>
|
||||
<button id="homeBtn" class="primary" aria-label="Go to Home">Go to Home</button>
|
||||
<button id="settingsBtn" aria-label="Open Settings">Open Settings</button>
|
||||
</div>
|
||||
<div class="mini">Nebula Navigation Error</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const params = new URLSearchParams(location.search);
|
||||
const attemptedUrl = params.get('url');
|
||||
const box = document.getElementById('targetBox');
|
||||
if (attemptedUrl) {
|
||||
box.textContent = decodeURIComponent(attemptedUrl);
|
||||
} else {
|
||||
box.textContent = 'Unknown URL';
|
||||
}
|
||||
|
||||
function sendNavigate(url, opts){
|
||||
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type:'navigate', url, opts }, '*');
|
||||
} else if (url === 'nebula://home') {
|
||||
window.location.href = 'home.html';
|
||||
} else if (url === 'nebula://settings') {
|
||||
window.location.href = 'settings.html';
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('backBtn').onclick = () => history.length > 1 ? history.back() : sendNavigate('nebula://home');
|
||||
document.getElementById('homeBtn').onclick = () => sendNavigate('nebula://home');
|
||||
document.getElementById('settingsBtn').onclick = () => window.location.href = "settings.html";
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,529 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nebula - Big Picture Mode</title>
|
||||
<link rel="icon" href="../assets/images/branding/Nebula-Icon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="../css/bigpicture.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3/fonts/remixicon.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Audio feedback for navigation -->
|
||||
<audio id="navSound" preload="auto">
|
||||
<source src="data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQ==" type="audio/wav">
|
||||
</audio>
|
||||
<audio id="selectSound" preload="auto">
|
||||
<source src="data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQ==" type="audio/wav">
|
||||
</audio>
|
||||
|
||||
<!-- Main container with ambient background -->
|
||||
<div class="bp-container">
|
||||
<!-- Animated background -->
|
||||
<div class="bp-background">
|
||||
<div class="bg-gradient"></div>
|
||||
<div class="bg-particles"></div>
|
||||
<div class="bg-glow"></div>
|
||||
</div>
|
||||
|
||||
<!-- Top header bar -->
|
||||
<header class="bp-header">
|
||||
<div class="header-left">
|
||||
<img src="../assets/images/branding/Nebula-Icon.svg" alt="Nebula" class="bp-logo">
|
||||
<span class="bp-title">Nebula</span>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="clock-widget">
|
||||
<span id="bp-time" class="time">--:--</span>
|
||||
<span id="bp-date" class="date">---</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="status-icons">
|
||||
<span id="bp-wifi" class="status-icon" title="Connected">
|
||||
<span class="material-symbols-outlined">wifi</span>
|
||||
</span>
|
||||
<span id="bp-battery" class="status-icon" title="Battery">
|
||||
<span class="material-symbols-outlined">battery_full</span>
|
||||
</span>
|
||||
</div>
|
||||
<button id="exitBigPicture" class="bp-exit-btn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
<span class="btn-label">Exit</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main navigation area -->
|
||||
<main class="bp-main">
|
||||
<!-- Left sidebar navigation -->
|
||||
<nav class="bp-sidebar">
|
||||
<div class="nav-items">
|
||||
<button class="nav-item active" data-section="home" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">home</span>
|
||||
<span class="nav-label">Home</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="browse" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">language</span>
|
||||
<span class="nav-label">Browse</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="bookmarks" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">bookmarks</span>
|
||||
<span class="nav-label">Bookmarks</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="history" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">history</span>
|
||||
<span class="nav-label">History</span>
|
||||
</button>
|
||||
<button class="nav-item" data-section="downloads" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
<span class="nav-label">Downloads</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="nav-footer">
|
||||
<button class="nav-item" data-section="settings" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="nav-label">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="bp-content">
|
||||
<!-- Webview container for browsing -->
|
||||
<div id="webview-container" class="webview-container hidden"></div>
|
||||
|
||||
<!-- Home section -->
|
||||
<section id="section-home" class="bp-section active">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">
|
||||
<span id="greeting-text">Welcome back</span>
|
||||
</h1>
|
||||
<p class="section-subtitle">What would you like to browse today?</p>
|
||||
</div>
|
||||
|
||||
<!-- Search card -->
|
||||
<div class="search-card" data-focusable tabindex="0">
|
||||
<div class="search-icon">
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
</div>
|
||||
<input type="text" id="bp-search" class="search-input" placeholder="Search the web or enter URL..." autocomplete="off">
|
||||
<div class="search-hint">
|
||||
<span class="key-hint">A</span> Search
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick access grid -->
|
||||
<div class="quick-access">
|
||||
<h2 class="subsection-title">Quick Access</h2>
|
||||
<div class="tile-grid" id="quickAccessGrid">
|
||||
<!-- Tiles will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent sites -->
|
||||
<div class="recent-sites">
|
||||
<h2 class="subsection-title">Continue Browsing</h2>
|
||||
<div class="horizontal-scroll" id="recentSitesScroll">
|
||||
<!-- Recent sites will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Browse section (for webview) -->
|
||||
<section id="section-browse" class="bp-section">
|
||||
<!-- Webview container is outside sections -->
|
||||
</section>
|
||||
|
||||
<!-- Bookmarks section -->
|
||||
<section id="section-bookmarks" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">Bookmarks</h1>
|
||||
<p class="section-subtitle">Your saved websites</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<button class="action-btn" id="addBookmarkBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">bookmark_add</span>
|
||||
<span>Add Bookmark</span>
|
||||
</button>
|
||||
<button class="action-btn" id="addCurrentBookmarkBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">bookmark</span>
|
||||
<span>Add Current Page</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tile-grid large" id="bookmarksGrid">
|
||||
<!-- Bookmarks will be populated dynamically -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- History section -->
|
||||
<section id="section-history" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">History</h1>
|
||||
<p class="section-subtitle">Recently visited sites</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<button class="action-btn" id="clearHistoryBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete_sweep</span>
|
||||
<span>Clear History</span>
|
||||
</button>
|
||||
<button class="action-btn" id="refreshHistoryBtn" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="list-container" id="historyList">
|
||||
<!-- History will be populated dynamically -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Downloads section -->
|
||||
<section id="section-downloads" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">Downloads</h1>
|
||||
<p class="section-subtitle">Your downloaded files</p>
|
||||
</div>
|
||||
<div class="list-container" id="downloadsList">
|
||||
<div class="empty-state">
|
||||
<span class="material-symbols-outlined">folder_open</span>
|
||||
<p>No recent downloads</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- NeBot AI section -->
|
||||
<section id="section-nebot" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">NeBot AI Assistant</h1>
|
||||
<p class="section-subtitle">Your AI-powered browsing companion</p>
|
||||
</div>
|
||||
<div class="nebot-launch">
|
||||
<div class="nebot-card" data-focusable tabindex="0" id="launchNebot">
|
||||
<div class="nebot-icon">
|
||||
<span class="material-symbols-outlined">smart_toy</span>
|
||||
</div>
|
||||
<div class="nebot-info">
|
||||
<h3>Start Conversation</h3>
|
||||
<p>Ask questions, get summaries, and more</p>
|
||||
</div>
|
||||
<div class="nebot-action">
|
||||
<span class="key-hint">A</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings section -->
|
||||
<section id="section-settings" class="bp-section">
|
||||
<div class="section-header">
|
||||
<h1 class="section-title">Settings</h1>
|
||||
<p class="section-subtitle">Configure your browser</p>
|
||||
</div>
|
||||
|
||||
<!-- Settings categories navigation -->
|
||||
<div class="settings-tabs">
|
||||
<button class="settings-tab active" data-settings-tab="themes" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">palette</span>
|
||||
<span>Themes</span>
|
||||
</button>
|
||||
<button class="settings-tab" data-settings-tab="display" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">display_settings</span>
|
||||
<span>Display</span>
|
||||
</button>
|
||||
<button class="settings-tab" data-settings-tab="privacy" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">shield</span>
|
||||
<span>Privacy</span>
|
||||
</button>
|
||||
<button class="settings-tab" data-settings-tab="about" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">info</span>
|
||||
<span>About</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings panels -->
|
||||
<div class="settings-panels">
|
||||
<!-- Themes Panel -->
|
||||
<div class="settings-panel active" id="settings-panel-themes">
|
||||
<h2 class="settings-panel-title">Theme Presets</h2>
|
||||
<div class="theme-grid" id="bp-theme-grid">
|
||||
<button class="theme-card" data-theme="default" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #121418, #1B1035);"></div>
|
||||
<span class="theme-name">Default</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="ocean" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1a365d, #2c5282);"></div>
|
||||
<span class="theme-name">Ocean</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="forest" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1a202c, #2d3748);"></div>
|
||||
<span class="theme-name">Forest</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="sunset" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #744210, #c05621);"></div>
|
||||
<span class="theme-name">Sunset</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="cyberpunk" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0a0a0a, #2a0a3a);"></div>
|
||||
<span class="theme-name">Cyberpunk</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="midnight-rose" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1c1820, #3d3046);"></div>
|
||||
<span class="theme-name">Midnight Rose</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="arctic-ice" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #f0f8ff, #d1e7ff);"></div>
|
||||
<span class="theme-name">Arctic Ice</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="cherry-blossom" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #fff5f8, #ffd4db);"></div>
|
||||
<span class="theme-name">Cherry Blossom</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="cosmic-purple" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0f0524, #2d1b69);"></div>
|
||||
<span class="theme-name">Cosmic Purple</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="emerald-dream" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0d2818, #2d5a44);"></div>
|
||||
<span class="theme-name">Emerald Dream</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="mocha-coffee" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #3c2414, #5d3a26);"></div>
|
||||
<span class="theme-name">Mocha Coffee</span>
|
||||
</button>
|
||||
<button class="theme-card" data-theme="lavender-fields" data-focusable tabindex="0">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #f8f4ff, #e6d8ff);"></div>
|
||||
<span class="theme-name">Lavender Fields</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Panel -->
|
||||
<div class="settings-panel" id="settings-panel-display">
|
||||
<h2 class="settings-panel-title">Display Settings</h2>
|
||||
<div class="settings-option">
|
||||
<div class="option-info">
|
||||
<span class="material-symbols-outlined">zoom_in</span>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Display Scale</span>
|
||||
<span class="option-description">Adjust the default zoom level</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-control">
|
||||
<button class="scale-btn" id="bp-scale-down" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">remove</span>
|
||||
</button>
|
||||
<span class="scale-value" id="bp-scale-value">100%</span>
|
||||
<button class="scale-btn" id="bp-scale-up" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-option">
|
||||
<div class="option-info">
|
||||
<span class="material-symbols-outlined">desktop_windows</span>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Exit Big Picture Mode</span>
|
||||
<span class="option-description">Return to standard desktop interface</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-control">
|
||||
<button class="action-button" id="bp-exit-desktop" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span>Exit</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy Panel -->
|
||||
<div class="settings-panel" id="settings-panel-privacy">
|
||||
<h2 class="settings-panel-title">Privacy & Data</h2>
|
||||
<div class="settings-option">
|
||||
<div class="option-info">
|
||||
<span class="material-symbols-outlined">delete_sweep</span>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Clear Browsing Data</span>
|
||||
<span class="option-description">Delete cookies, cache, and site data</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-control">
|
||||
<button class="action-button danger" id="bp-clear-data" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
<span>Clear All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-option">
|
||||
<div class="option-info">
|
||||
<span class="material-symbols-outlined">history</span>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Clear History</span>
|
||||
<span class="option-description">Delete browsing history</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-control">
|
||||
<button class="action-button" id="bp-clear-history" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-option">
|
||||
<div class="option-info">
|
||||
<span class="material-symbols-outlined">search_off</span>
|
||||
<div class="option-text">
|
||||
<span class="option-label">Clear Search History</span>
|
||||
<span class="option-description">Delete search query history</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-control">
|
||||
<button class="action-button" id="bp-clear-search" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Panel -->
|
||||
<div class="settings-panel" id="settings-panel-about">
|
||||
<h2 class="settings-panel-title">About Nebula Browser</h2>
|
||||
<div class="about-info">
|
||||
<div class="about-logo">
|
||||
<img src="../assets/images/branding/Nebula-Icon.svg" alt="Nebula" class="about-logo-img">
|
||||
<div class="about-title">
|
||||
<h3>Nebula Browser</h3>
|
||||
<span id="bp-version">Version loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="about-details">
|
||||
<div class="about-row">
|
||||
<span class="about-label">CEF</span>
|
||||
<span class="about-value" id="bp-cef-version">--</span>
|
||||
</div>
|
||||
<div class="about-row">
|
||||
<span class="about-label">Chromium</span>
|
||||
<span class="about-value" id="bp-chromium-version">--</span>
|
||||
</div>
|
||||
<div class="about-row">
|
||||
<span class="about-label">Node.js</span>
|
||||
<span class="about-value" id="bp-node-version">--</span>
|
||||
</div>
|
||||
<div class="about-row">
|
||||
<span class="about-label">Platform</span>
|
||||
<span class="about-value" id="bp-platform">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="about-actions">
|
||||
<button class="action-button" id="bp-github-link" data-focusable tabindex="0">
|
||||
<span class="ri-github-fill" style="font-size: 20px;"></span>
|
||||
<span>GitHub</span>
|
||||
</button>
|
||||
<button class="action-button" id="bp-copy-diagnostics" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">content_copy</span>
|
||||
<span>Copy Info</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Bottom controller hints -->
|
||||
<footer class="bp-footer">
|
||||
<div class="controller-hints">
|
||||
<div class="hint">
|
||||
<span class="controller-btn dpad">
|
||||
<span class="material-symbols-outlined">gamepad</span>
|
||||
</span>
|
||||
<span>Navigate</span>
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="controller-btn a-btn">A</span>
|
||||
<span>Select</span>
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="controller-btn b-btn">B</span>
|
||||
<span>Back</span>
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="controller-btn y-btn">Y</span>
|
||||
<span>Search</span>
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="controller-btn menu-btn">☰</span>
|
||||
<span>Menu</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- On-screen keyboard (for controller input) -->
|
||||
<div id="osk-overlay" class="osk-overlay hidden">
|
||||
<div class="osk-container">
|
||||
<div class="osk-title">
|
||||
<span class="material-symbols-outlined">keyboard</span>
|
||||
<span id="osk-label">Enter text</span>
|
||||
</div>
|
||||
<div class="osk-header">
|
||||
<div class="osk-input-wrapper">
|
||||
<input type="text" id="osk-input" class="osk-text-input" placeholder="Your text appears here..." readonly>
|
||||
<span id="osk-cursor" class="osk-cursor"></span>
|
||||
<span id="osk-text-measure" class="osk-text-measure"></span>
|
||||
</div>
|
||||
<button class="osk-close" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="osk-keyboard" id="osk-keyboard">
|
||||
<!-- Keyboard rows will be generated by JS -->
|
||||
</div>
|
||||
<div class="osk-actions">
|
||||
<button class="osk-action-btn" id="osk-space" data-focusable tabindex="0">
|
||||
<span class="btn-hint">Y</span> Space
|
||||
</button>
|
||||
<button class="osk-action-btn" id="osk-backspace" data-focusable tabindex="0">
|
||||
<span class="btn-hint">X</span>
|
||||
<span class="material-symbols-outlined">backspace</span>
|
||||
</button>
|
||||
<button class="osk-action-btn" id="osk-clear" data-focusable tabindex="0">
|
||||
<span class="btn-hint">LB</span> Clear
|
||||
</button>
|
||||
<button class="osk-action-btn primary" id="osk-submit" data-focusable tabindex="0">
|
||||
<span class="btn-hint">RB</span> Go
|
||||
</button>
|
||||
</div>
|
||||
<div class="osk-hints">
|
||||
<span><b>A</b> Type</span>
|
||||
<span><b>X</b> Backspace</span>
|
||||
<span><b>Y</b> Space</span>
|
||||
<span><b>B</b> Close</span>
|
||||
<span><b>LB</b> Clear All</span>
|
||||
<span><b>RB</b> Submit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context menu -->
|
||||
<div id="context-menu" class="context-menu hidden">
|
||||
<button class="context-item" data-action="open" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">open_in_new</span>
|
||||
<span>Open</span>
|
||||
</button>
|
||||
<button class="context-item" data-action="edit" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">edit</span>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button class="context-item" data-action="delete" data-focusable tabindex="0">
|
||||
<span class="material-symbols-outlined">delete</span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/bigpicture.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Downloads</title>
|
||||
<link rel="stylesheet" href="../css/style.css" />
|
||||
<link rel="stylesheet" href="../css/performance.css" />
|
||||
<style>
|
||||
body { font-family: system-ui, Segoe UI, Roboto, sans-serif; margin: 16px; color: #eee; background: #121212; }
|
||||
h1 { font-size: 20px; margin: 0 0 12px; }
|
||||
.download-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.download-item { background: #1e1e1e; border: 1px solid #2a2a2a; border-radius: 8px; padding: 10px 12px; display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; align-items: center; }
|
||||
.file { font-weight: 600; color: #fafafa; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.meta { font-size: 12px; color: #bbb; }
|
||||
.progress { height: 6px; background: #2a2a2a; border-radius: 4px; overflow: hidden; grid-column: 1 / -1; }
|
||||
.bar { height: 100%; background: #3b82f6; width: 0%; transition: width .15s linear; }
|
||||
.actions { display: flex; gap: 6px; }
|
||||
button { background: #2b2b2b; border: 1px solid #3a3a3a; color: #eee; border-radius: 6px; padding: 6px 10px; cursor: pointer; }
|
||||
button:hover { background: #333; }
|
||||
.state { font-size: 12px; color: #aaa; }
|
||||
.row { display: flex; gap: 12px; justify-content: space-between; align-items: center; }
|
||||
.empty { color: #888; font-style: italic; padding: 20px; text-align: center; }
|
||||
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||
.scan { font-size: 12px; }
|
||||
.scan.bad { color: #f87171; }
|
||||
.scan.good { color: #34d399; }
|
||||
.scan.pending { color: #fbbf24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<h1>Downloads</h1>
|
||||
<div>
|
||||
<button id="clear-completed">Clear Completed</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="list" class="download-list"></div>
|
||||
<div id="empty" class="empty" style="display:none;">No downloads yet</div>
|
||||
|
||||
<script>
|
||||
const api = window.downloadsAPI || null;
|
||||
const listEl = document.getElementById('list');
|
||||
const emptyEl = document.getElementById('empty');
|
||||
const clearBtn = document.getElementById('clear-completed');
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (!n || n <= 0) return '0 B';
|
||||
const u = ['B','KB','MB','GB','TB'];
|
||||
const i = Math.floor(Math.log(n)/Math.log(1024));
|
||||
return (n/Math.pow(1024,i)).toFixed( i===0 ? 0 : 1 ) + ' ' + u[i];
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return (s || '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[c]);
|
||||
}
|
||||
|
||||
function rowHtml(d){
|
||||
const pct = d.totalBytes > 0 ? Math.min(100, Math.round((d.receivedBytes||0) * 100 / d.totalBytes)) : 0;
|
||||
const scan = d.scan || { status: 'unavailable' };
|
||||
const isInfected = scan.status === 'infected';
|
||||
const isScanning = scan.status === 'scanning';
|
||||
const scanCls = scan.status === 'infected' ? 'scan bad' : (scan.status === 'clean' ? 'scan good' : (scan.status==='scanning'?'scan pending':'scan'));
|
||||
const scanText = scan.status === 'infected' ? `Threat detected (${scan.engine||''})` :
|
||||
scan.status === 'clean' ? `Scanned clean (${scan.engine||''})` :
|
||||
scan.status === 'scanning' ? `Scanning... (${scan.engine||''})` :
|
||||
scan.status === 'pending' ? `Queued for scan (${scan.engine||''})` :
|
||||
scan.status === 'error' ? `Scan error${scan.details?': '+esc(scan.details):''}` :
|
||||
'Scan unavailable';
|
||||
return `
|
||||
<div class="download-item" id="dl-${d.id}">
|
||||
<div class="file" title="${d.filename}">${d.filename}</div>
|
||||
<div class="actions">
|
||||
${d.state==='in-progress' ? `
|
||||
<button data-act="${d.paused?'resume':'pause'}" data-id="${d.id}">${d.paused?'Resume':'Pause'}</button>
|
||||
<button data-act="cancel" data-id="${d.id}">Cancel</button>
|
||||
` : `
|
||||
<button data-act="open-file" data-id="${d.id}" ${(d.state!=='completed'||isInfected)?'disabled':''}>Open</button>
|
||||
<button data-act="show-in-folder" data-id="${d.id}">Show in Folder</button>
|
||||
${isInfected ? `<button data-act="delete-file" data-id="${d.id}">Delete</button>` : ''}
|
||||
${d.state!=='in-progress' ? `<button data-act="rescan" data-id="${d.id}" ${isScanning?'disabled':''}>Rescan</button>` : ''}
|
||||
`}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="state">${d.state}</span>
|
||||
· ${fmtBytes(d.receivedBytes||0)} / ${fmtBytes(d.totalBytes||0)}
|
||||
· <span class="${scanCls}">${scanText}</span>
|
||||
</div>
|
||||
<div class="progress"><div class="bar" style="width:${pct}%"></div></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function render(entries){
|
||||
listEl.innerHTML = entries.map(rowHtml).join('');
|
||||
emptyEl.style.display = entries.length ? 'none' : 'block';
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
if (!api || typeof api.list !== 'function') {
|
||||
render([]);
|
||||
emptyEl.textContent = 'Downloads are managed by CEF in this build';
|
||||
return;
|
||||
}
|
||||
const all = await api.list();
|
||||
// Newest first by start time
|
||||
all.sort((a,b)=> (b.startedAt||0)-(a.startedAt||0));
|
||||
render(all);
|
||||
}
|
||||
|
||||
listEl.addEventListener('click', async (e)=>{
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
const id = btn.getAttribute('data-id');
|
||||
const act = btn.getAttribute('data-act');
|
||||
if (!id || !act) return;
|
||||
if (!api || typeof api.action !== 'function') return;
|
||||
await api.action(id, act);
|
||||
if (act==='cancel') refresh();
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', async ()=>{
|
||||
if (!api || typeof api.clearCompleted !== 'function') return;
|
||||
await api.clearCompleted();
|
||||
refresh();
|
||||
});
|
||||
|
||||
api?.onStarted?.(()=> refresh());
|
||||
api?.onUpdated?.(()=> {
|
||||
// Partial update: just patch progress bar and bytes if present
|
||||
// For simplicity now, refresh list
|
||||
refresh();
|
||||
});
|
||||
api?.onDone?.(()=> refresh());
|
||||
api?.onScanStarted?.(()=> refresh());
|
||||
api?.onScanResult?.(()=> refresh());
|
||||
api?.onCleared?.(()=> refresh());
|
||||
|
||||
refresh();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,231 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GPU Diagnostics - Nebula Browser</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.status.good { background: #d4edda; color: #155724; }
|
||||
.status.warning { background: #fff3cd; color: #856404; }
|
||||
.status.error { background: #f8d7da; color: #721c24; }
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover { background: #0056b3; }
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.canvas-test {
|
||||
border: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>GPU Diagnostics</h1>
|
||||
|
||||
<div id="gpu-status" class="status">
|
||||
<h3>GPU Status</h3>
|
||||
<p>Loading GPU information...</p>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>WebGL Test</h3>
|
||||
<canvas id="webgl-canvas" class="canvas-test" width="300" height="150"></canvas>
|
||||
<p id="webgl-status">Testing WebGL...</p>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h3>Canvas 2D Acceleration Test</h3>
|
||||
<canvas id="canvas2d" class="canvas-test" width="300" height="150"></canvas>
|
||||
<p id="canvas2d-status">Testing Canvas 2D...</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Actions</h3>
|
||||
<button onclick="refreshGPUInfo()">Refresh GPU Info</button>
|
||||
<button onclick="forceGC()">Force Garbage Collection</button>
|
||||
<button onclick="applyFallback(1)">Apply GPU Fallback Level 1</button>
|
||||
<button onclick="applyFallback(2)">Apply GPU Fallback Level 2</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Detailed GPU Information</h3>
|
||||
<pre id="gpu-details">Loading...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function refreshGPUInfo() {
|
||||
try {
|
||||
if (!window.electronAPI?.invoke) {
|
||||
const statusDiv = document.getElementById('gpu-status');
|
||||
const detailsDiv = document.getElementById('gpu-details');
|
||||
statusDiv.className = 'status warning';
|
||||
statusDiv.innerHTML = '<h3>GPU Status</h3><p>Native GPU diagnostics are not exposed to this CEF page.</p>';
|
||||
detailsDiv.textContent = navigator.userAgent;
|
||||
return;
|
||||
}
|
||||
const gpuInfo = await window.electronAPI.invoke('get-gpu-info');
|
||||
const statusDiv = document.getElementById('gpu-status');
|
||||
const detailsDiv = document.getElementById('gpu-details');
|
||||
|
||||
if (gpuInfo.error) {
|
||||
statusDiv.className = 'status error';
|
||||
statusDiv.innerHTML = `<h3>GPU Status</h3><p>Error: ${gpuInfo.error}</p>`;
|
||||
} else {
|
||||
const isGPUWorking = checkGPUFeatures(gpuInfo.featureStatus);
|
||||
statusDiv.className = `status ${isGPUWorking ? 'good' : 'warning'}`;
|
||||
statusDiv.innerHTML = `
|
||||
<h3>GPU Status</h3>
|
||||
<p><strong>Hardware Acceleration:</strong> ${isGPUWorking ? 'Enabled' : 'Disabled/Limited'}</p>
|
||||
<p><strong>Fallback Level:</strong> ${gpuInfo.fallbackStatus?.fallbackLevel || 0}</p>
|
||||
<p><strong>GPU Enabled:</strong> ${gpuInfo.fallbackStatus?.gpuEnabled ? 'Yes' : 'No'}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
detailsDiv.textContent = JSON.stringify(gpuInfo, null, 2);
|
||||
} catch (err) {
|
||||
console.error('Failed to get GPU info:', err);
|
||||
document.getElementById('gpu-status').innerHTML = `<h3>GPU Status</h3><p>Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function checkGPUFeatures(features) {
|
||||
const criticalFeatures = ['gpu_compositing', 'webgl', 'webgl2'];
|
||||
return criticalFeatures.some(feature =>
|
||||
features[feature] && !features[feature].includes('disabled')
|
||||
);
|
||||
}
|
||||
|
||||
async function forceGC() {
|
||||
try {
|
||||
if (!window.electronAPI?.invoke) {
|
||||
alert('Garbage collection is managed by CEF in this build.');
|
||||
return;
|
||||
}
|
||||
await window.electronAPI.invoke('force-gc');
|
||||
alert('Garbage collection completed');
|
||||
} catch (err) {
|
||||
alert('Failed to force GC: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFallback(level) {
|
||||
try {
|
||||
if (!window.electronAPI?.invoke) {
|
||||
alert('GPU fallback settings are managed by the native CEF app.');
|
||||
return;
|
||||
}
|
||||
const result = await window.electronAPI.invoke('apply-gpu-fallback', level);
|
||||
if (result.success) {
|
||||
alert(`Applied GPU fallback level ${level}. App restart may be required.`);
|
||||
} else {
|
||||
alert('Failed to apply fallback: ' + result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to apply fallback: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Test WebGL
|
||||
function testWebGL() {
|
||||
const canvas = document.getElementById('webgl-canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
const status = document.getElementById('webgl-status');
|
||||
|
||||
if (gl) {
|
||||
// Draw a simple triangle
|
||||
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vertexShader, `
|
||||
attribute vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`);
|
||||
gl.compileShader(vertexShader);
|
||||
|
||||
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(fragmentShader, `
|
||||
precision mediump float;
|
||||
void main() {
|
||||
gl_Color = vec4(0.0, 1.0, 0.0, 1.0);
|
||||
}
|
||||
`);
|
||||
gl.compileShader(fragmentShader);
|
||||
|
||||
status.textContent = 'WebGL: Available ✓';
|
||||
status.parentElement.className = 'status good';
|
||||
|
||||
// Clear with green color to show it's working
|
||||
gl.clearColor(0.0, 0.8, 0.0, 1.0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
} else {
|
||||
status.textContent = 'WebGL: Not Available ✗';
|
||||
status.parentElement.className = 'status error';
|
||||
}
|
||||
}
|
||||
|
||||
// Test Canvas 2D
|
||||
function testCanvas2D() {
|
||||
const canvas = document.getElementById('canvas2d');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const status = document.getElementById('canvas2d-status');
|
||||
|
||||
try {
|
||||
// Draw some graphics to test acceleration
|
||||
const gradient = ctx.createLinearGradient(0, 0, 300, 0);
|
||||
gradient.addColorStop(0, '#ff0000');
|
||||
gradient.addColorStop(1, '#0000ff');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 300, 150);
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.fillText('Canvas 2D Working!', 50, 80);
|
||||
|
||||
status.textContent = 'Canvas 2D: Working ✓';
|
||||
status.parentElement.className = 'status good';
|
||||
} catch (err) {
|
||||
status.textContent = 'Canvas 2D: Error - ' + err.message;
|
||||
status.parentElement.className = 'status error';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize tests
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
refreshGPUInfo();
|
||||
testWebGL();
|
||||
testCanvas2D();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>New Tab</title>
|
||||
<link rel="icon" href="../assets/images/branding/Nebula-Icon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="../css/home.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
|
||||
rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3/fonts/remixicon.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="home-container">
|
||||
<button id="editLayoutBtn" class="edit-btn" aria-pressed="false" title="Edit layout">Edit</button>
|
||||
<!-- Dynamic greeting replaces the large logo for a cleaner hero look -->
|
||||
<h1 id="greeting" class="greeting-title">Welcome</h1>
|
||||
|
||||
<!-- Retain logo for branding but keep it subtle/optional -->
|
||||
<div class="logo" aria-hidden="true" style="display:none">
|
||||
<img src="../assets/images/branding/Nebula-Logo.svg" class="logo-img">
|
||||
<div class="logo-text">Nebula Browser</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-engine-selector">
|
||||
<button id="searchEngineBtn" class="search-engine-btn">
|
||||
<img id="searchEngineLogo" src="../assets/icons/searchengines/google.svg" alt="Search Engine">
|
||||
</button>
|
||||
<div id="searchEngineDropdown" class="search-engine-dropdown hidden">
|
||||
<div class="search-engine-option" data-engine="google">
|
||||
<img src="../assets/icons/searchengines/google.svg" alt="Google">
|
||||
</div>
|
||||
<div class="search-engine-option" data-engine="bing">
|
||||
<img src="../assets/icons/searchengines/Bing.svg" alt="Bing">
|
||||
</div>
|
||||
<div class="search-engine-option" data-engine="duckduckgo">
|
||||
<img src="../assets/icons/searchengines/duckduckgo.svg" alt="DuckDuckGo">
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Top Sites card -->
|
||||
<section class="top-sites-card">
|
||||
<header class="top-sites-header">
|
||||
<h2>Bookmarks</h2>
|
||||
<button id="resetTopSites" class="link-btn" title="Clear bookmarks">Reset</button>
|
||||
</header>
|
||||
<div class="bookmarks" id="bookmarkList">
|
||||
<!-- Bookmarks dynamically inserted here -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- At a glance widget -->
|
||||
<aside class="glance" aria-live="polite">
|
||||
<div class="glance-card">
|
||||
<div class="glance-title">At A Glance</div>
|
||||
<div class="glance-grid">
|
||||
<div class="glance-tile">
|
||||
<div class="glance-label">Time</div>
|
||||
<div id="clock" class="glance-value">--:--</div>
|
||||
</div>
|
||||
<div class="glance-tile">
|
||||
<div class="glance-label">Weather</div>
|
||||
<div id="weather" class="glance-value">Fetching…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Edit mode toolbar -->
|
||||
<div id="editToolbar" class="edit-toolbar" hidden>
|
||||
<label style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
||||
<input type="checkbox" id="toggleShowGreeting" checked>
|
||||
<span>Show greeting</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;margin-right:8px;">
|
||||
<input type="checkbox" id="toggleShowBookmarks" checked>
|
||||
<span>Show bookmarks</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;margin-right:12px;">
|
||||
<input type="checkbox" id="toggleShowGlance" checked>
|
||||
<span>Show At a Glance</span>
|
||||
</label>
|
||||
<button id="cancelEditBtn" class="btn secondary" aria-label="Cancel layout edits">Cancel</button>
|
||||
<button id="saveEditBtn" class="btn primary" aria-label="Save layout edits">Save</button>
|
||||
</div>
|
||||
|
||||
<!-- Popup for adding a bookmark -->
|
||||
<div id="addPopup" class="popup hidden">
|
||||
<div class="popup-inner">
|
||||
<h2>Add New Bookmark</h2>
|
||||
|
||||
<!-- Title field -->
|
||||
<label for="titleInput">Title</label>
|
||||
<input type="text" id="titleInput" placeholder="Enter title">
|
||||
|
||||
<!-- URL field -->
|
||||
<label for="urlInput">URL</label>
|
||||
<input type="url" id="urlInput" placeholder="https://example.com">
|
||||
|
||||
<!-- Icon picker -->
|
||||
<div class="icon-picker-layout">
|
||||
<nav class="icon-side-nav" id="iconCategoryNav" aria-label="Icon Categories">
|
||||
<!-- Category nav buttons injected here -->
|
||||
</nav>
|
||||
<div class="icon-main">
|
||||
<div class="icon-header">
|
||||
<label for="iconFilter" class="icon-filter-label">Icon</label>
|
||||
<div class="favicon-toggle">
|
||||
<input type="checkbox" id="useFavicon" class="favicon-checkbox">
|
||||
<label for="useFavicon" class="favicon-label">Use site favicon</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="iconFilter" class="icon-filter"
|
||||
placeholder="Search for icon, enter emoji, or type 'favicon'">
|
||||
<div id="iconGrid" class="icon-grid" tabindex="0" aria-label="Icon selection list"></div>
|
||||
<input type="hidden" id="selectedIcon">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- action buttons -->
|
||||
<div class="popup-buttons">
|
||||
<button id="cancelBtn">Cancel</button>
|
||||
<button id="saveBookmarkBtn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme loader script -->
|
||||
<script src="../js/customization.js"></script>
|
||||
<script>
|
||||
// Apply saved theme on page load and listen for updates
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
BrowserCustomizer.applyThemeToPage();
|
||||
|
||||
// Function to update logo and title based on theme
|
||||
function updateLogoAndTitle(theme) {
|
||||
const logoText = document.querySelector('.logo-text');
|
||||
const logoImg = document.querySelector('.logo-img');
|
||||
if (logoText) {
|
||||
logoText.textContent = theme.customTitle || 'Nebula Browser';
|
||||
}
|
||||
if (logoImg) {
|
||||
logoImg.style.display = theme.showLogo ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for theme updates via postMessage fallback
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'theme-update') {
|
||||
const theme = event.data.theme;
|
||||
localStorage.setItem('currentTheme', JSON.stringify(theme));
|
||||
BrowserCustomizer.applyThemeToPage();
|
||||
updateLogoAndTitle(theme);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for theme updates when a native bridge provides them.
|
||||
if (window.electronAPI && typeof window.electronAPI.on === 'function') {
|
||||
window.electronAPI.on('theme-update', (theme) => {
|
||||
localStorage.setItem('currentTheme', JSON.stringify(theme));
|
||||
BrowserCustomizer.applyThemeToPage();
|
||||
updateLogoAndTitle(theme);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- make this a module so we can import icons -->
|
||||
<script type="module" src="../js/home.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nebula Browser</title>
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
</head>
|
||||
<body class="cef-shell">
|
||||
<main class="cef-start">
|
||||
<section class="cef-card">
|
||||
<p class="eyebrow">Nebula Browser</p>
|
||||
<h1>Browse with CEF</h1>
|
||||
<p class="lede">This page is now a lightweight start surface. Tabs, windows, and page hosting are handled by the CEF application.</p>
|
||||
|
||||
<form id="start-form" class="start-search" autocomplete="off">
|
||||
<label class="sr-only" for="start-url">Search or enter address</label>
|
||||
<input id="start-url" type="text" placeholder="Search or enter address" autofocus>
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
|
||||
<nav class="quick-links" aria-label="Nebula pages">
|
||||
<a href="home.html">Home</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a href="downloads.html">Downloads</a>
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="../js/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Connection Not Secure</title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style>
|
||||
:root {
|
||||
--bg:#121212; --panel:#1e1e1e; --warn:#d97706; --danger:#dc2626; --text:#f5f5f5; --muted:#9ca3af; --accent:#6366f1;
|
||||
color-scheme: dark;
|
||||
}
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Inter,Ubuntu,sans-serif; background:var(--bg); color:var(--text); display:flex; min-height:100vh; align-items:center; justify-content:center; padding:32px; }
|
||||
.card { max-width:780px; width:100%; background:linear-gradient(145deg,#1c1c1c,#242424); border:1px solid #2c2c2c; border-radius:20px; padding:40px 46px 48px; box-shadow:0 8px 28px -6px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,0.04); position:relative; overflow:hidden; }
|
||||
.card:before { content:""; position:absolute; inset:0; background:radial-gradient(circle at 18% 15%,rgba(255,255,255,.08),transparent 55%), radial-gradient(circle at 82% 78%,rgba(255,255,255,.05),transparent 60%); pointer-events:none; }
|
||||
h1 { font-size: clamp(1.9rem, 2.6vw, 2.6rem); margin:0 0 12px; letter-spacing:-.5px; display:flex; align-items:center; gap:.6rem; }
|
||||
h1 span.badge { font-size:12px; letter-spacing:1px; padding:4px 8px; border:1px solid var(--warn); color:var(--warn); border-radius:999px; text-transform:uppercase; background:rgba(217,119,6,0.1); }
|
||||
p.lede { font-size:1.05rem; line-height:1.55; margin:0 0 22px; color:var(--muted); }
|
||||
code { background:#252525; padding:3px 6px; border-radius:6px; font-size:.9rem; color:#e0e0e0; }
|
||||
.url-box { font-family:monospace; font-size:.92rem; padding:10px 12px; background:#181818; border:1px solid #2a2a2a; border-radius:10px; word-break:break-all; margin:0 0 22px; display:flex; align-items:center; gap:.75rem; }
|
||||
.url-box svg { flex:0 0 auto; width:22px; height:22px; stroke:var(--warn); }
|
||||
ul { margin:0 0 26px 1.1rem; padding:0; line-height:1.5; color:var(--muted); }
|
||||
ul li { margin-bottom:6px; }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:14px; }
|
||||
button { cursor:pointer; font-size:.95rem; letter-spacing:.4px; font-weight:500; border-radius:12px; padding:14px 26px; border:1px solid transparent; background:linear-gradient(135deg,#303030,#252525); color:#fff; position:relative; overflow:hidden; transition:.25s; }
|
||||
button.primary { background:linear-gradient(135deg,#6366f1,#5145cd); box-shadow:0 4px 18px -4px rgba(99,102,241,.5); }
|
||||
button.danger { background:linear-gradient(135deg,#b91c1c,#7f1d1d); border-color:#dc2626; }
|
||||
button.outline { background:transparent; border-color:#444; }
|
||||
button:hover { filter:brightness(1.12); transform:translateY(-2px); }
|
||||
button:active { transform:translateY(0); filter:brightness(.9); }
|
||||
.mini { font-size:.75rem; text-transform:uppercase; letter-spacing:1px; opacity:.8; margin-top:24px; }
|
||||
.fade-in { animation:fade .5s ease .05s both; }
|
||||
@keyframes fade { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform:none; } }
|
||||
.grid { display:grid; gap:40px; }
|
||||
@media (max-width:760px){ .card{padding:34px 28px 40px;} h1{font-size:2rem;} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card fade-in">
|
||||
<h1>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M12 2 2 7l10 5 10-5-10-5Z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
Connection Not Secure <span class="badge">http</span>
|
||||
</h1>
|
||||
<p class="lede">You’re about to visit a page using <strong>HTTP (unencrypted)</strong>. Information you send or view can potentially be intercepted or modified. If this is a site you trust and you understand the risks, you can continue anyway.</p>
|
||||
<div class="url-box" id="targetBox" title="Target URL"></div>
|
||||
<ul>
|
||||
<li>No TLS encryption – data (including passwords or forms) travels in plain text.</li>
|
||||
<li>Attackers on the same network (café Wi‑Fi, school, workplace) could tamper with or read content.</li>
|
||||
<li>The site might support HTTPS. Try manually changing to <code>https://</code> first.</li>
|
||||
<li>Proceed only if necessary and you have a reason to trust this destination.</li>
|
||||
</ul>
|
||||
<div class="actions">
|
||||
<button id="backBtn" class="outline" aria-label="Go Back">Go Back</button>
|
||||
<button id="tryHttps" class="primary" aria-label="Retry with HTTPS">Try HTTPS</button>
|
||||
<button id="continueBtn" class="danger" aria-label="Continue (HTTP)">Continue Anyway</button>
|
||||
</div>
|
||||
<div class="mini">Nebula Secure Navigation Interstitial</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const params = new URLSearchParams(location.search);
|
||||
const target = params.get('target');
|
||||
const box = document.getElementById('targetBox');
|
||||
if (target) box.textContent = target;
|
||||
function sendNavigate(url, opts){
|
||||
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type:'navigate', url, opts }, '*');
|
||||
} else if (url === 'nebula://home') {
|
||||
window.location.href = 'home.html';
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
document.getElementById('backBtn').onclick = () => history.length > 1 ? history.back() : sendNavigate('nebula://home');
|
||||
document.getElementById('tryHttps').onclick = () => {
|
||||
if (!target) return; try {
|
||||
const u = new URL(target.replace(/^http:/,'https:'));
|
||||
sendNavigate(u.href);
|
||||
} catch { sendNavigate(target.replace(/^http:/,'https:')); }
|
||||
};
|
||||
document.getElementById('continueBtn').onclick = () => {
|
||||
if (!target) return; sendNavigate(target, { insecureBypass:true });
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Menu</title>
|
||||
<link rel="stylesheet" href="../css/menu-popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="menu-popup" role="menu">
|
||||
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
||||
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
|
||||
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
|
||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
||||
<span id="zoom-percent">100%</span>
|
||||
<button data-cmd="zoom-in" aria-label="Zoom in">+</button>
|
||||
</div>
|
||||
<button data-cmd="hard-reload" role="menuitem">Hard Reload (Ignore Cache)</button>
|
||||
<button data-cmd="fresh-reload" role="menuitem">Reload Fresh (Add Cache-Buster)</button>
|
||||
</div>
|
||||
<script src="../js/menu-popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Nebot</title>
|
||||
<link rel="stylesheet" href="../plugins/nebot/page.css" onerror="this.remove()"/>
|
||||
<style>
|
||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:#12141c; color:#e6e8ef; }
|
||||
.fallback { max-width:620px; margin:60px auto; padding:32px 36px; background:#1d222e; border:1px solid rgba(255,255,255,0.08); border-radius:18px; }
|
||||
.fallback h1 { margin:0 0 12px; font-size:28px; background:linear-gradient(90deg,#a48bff,#6cb6ff); -webkit-background-clip:text; color:transparent; }
|
||||
.fallback p { line-height:1.55; }
|
||||
.tip { background:#232b38; padding:10px 14px; border-radius:10px; font-size:13px; margin-top:18px; border:1px solid rgba(255,255,255,0.08); }
|
||||
.err { color:#ff6d7d; font-weight:600; }
|
||||
#mount { min-height:400px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mount"></div>
|
||||
<script>
|
||||
(async function(){
|
||||
// Wait a tick so plugin preload (renderer-preload) runs and exposes window.ollamaChat
|
||||
const mount = document.getElementById('mount');
|
||||
function showFallback(reason){
|
||||
mount.innerHTML = `<div class="fallback">`+
|
||||
`<h1>Nebot</h1>`+
|
||||
`<p>The Nebot plugin page could not load automatically.</p>`+
|
||||
(reason?`<p class='err'>${reason}</p>`:'')+
|
||||
`<div class="tip"><strong>How to fix</strong><br/>1. Ensure the Nebot plugin folder exists at plugins/nebot.<br/>2. Confirm plugin is enabled (manifest enabled: true).<br/>3. Restart the app so the plugin manager registers pages.</div>`+
|
||||
`</div>`;
|
||||
}
|
||||
try {
|
||||
// Try to fetch plugin page HTML directly
|
||||
const res = await fetch('../plugins/nebot/page.html');
|
||||
if(!res.ok){ showFallback('Missing page.html (status '+res.status+').'); return; }
|
||||
const html = await res.text();
|
||||
// Simple sandboxed injection
|
||||
mount.innerHTML = html;
|
||||
// The injected page expects its CSS & JS relative to itself; adjust asset paths
|
||||
const fixLinks = mount.querySelectorAll('link[rel="stylesheet"], script[src]');
|
||||
fixLinks.forEach(el=>{
|
||||
const attr = el.tagName==='SCRIPT'?'src':'href';
|
||||
if(el.getAttribute(attr) && !/plugins\/nebot\//.test(el.getAttribute(attr))){
|
||||
el.setAttribute(attr,'../plugins/nebot/'+el.getAttribute(attr));
|
||||
}
|
||||
});
|
||||
// Inject JS if not already present
|
||||
if(!mount.querySelector('script[data-nebot-page]')){
|
||||
const s=document.createElement('script'); s.dataset.nebotPage='1';
|
||||
// Pass the current URL hash to the page script for debug mode
|
||||
s.src='../plugins/nebot/page.js' + window.location.hash;
|
||||
mount.appendChild(s);
|
||||
}
|
||||
} catch(e){
|
||||
showFallback(e.message||'Unknown error');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,596 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Settings</title>
|
||||
<link rel="stylesheet" href="../css/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>">
|
||||
<!-- Inline styles removed; styles now live in settings.css for a cleaner, modern look. -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" role="application">
|
||||
<aside class="sidebar" aria-label="Settings categories">
|
||||
<h1>Settings</h1>
|
||||
<nav class="tabs" role="tablist">
|
||||
<button class="tab-link active" role="tab" aria-selected="true" aria-controls="panel-general" id="tab-general" data-tab="general">General</button>
|
||||
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-appearance" id="tab-appearance" data-tab="appearance">Appearance</button>
|
||||
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-history" id="tab-history" data-tab="history">History</button>
|
||||
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-plugins" id="tab-plugins" data-tab="plugins">Plugins</button>
|
||||
<button class="tab-link" role="tab" aria-selected="false" aria-controls="panel-about" id="tab-about" data-tab="about">About</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
<!-- General Panel -->
|
||||
<section class="tab-panel active" id="panel-general" role="tabpanel" aria-labelledby="tab-general">
|
||||
<h2>General</h2>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>Data Management</h3>
|
||||
<p class="note">Clear all cookies, cache, and browsing data stored locally on this device.</p>
|
||||
<div class="setting-row">
|
||||
<button id="clear-data-btn">Clear All Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Big Picture Mode -->
|
||||
<div class="setting-group">
|
||||
<h3>Big Picture Mode</h3>
|
||||
<p class="note">A controller-friendly UI designed for handheld devices (e.g., Steam Deck).</p>
|
||||
<div class="setting-row">
|
||||
<button id="launch-bigpicture-btn" class="primary-btn">
|
||||
<span style="font-size: 18px;"></span> Launch Big Picture Mode
|
||||
</button>
|
||||
<span id="bigpicture-status" class="note"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>Weather Display</h3>
|
||||
<p class="note">Choose how temperature is displayed on the Home page weather card.</p>
|
||||
<fieldset class="weather-units settings-fieldset">
|
||||
<legend>Temperature units</legend>
|
||||
<label style="display: block; margin-bottom: 8px;"><input type="radio" name="weather-unit" id="weather-unit-auto" value="auto" checked> Auto (based on locale)</label>
|
||||
<label style="display: block; margin-bottom: 8px;"><input type="radio" name="weather-unit" id="weather-unit-c" value="c"> Celsius (°C)</label>
|
||||
<label style="display: block; margin-bottom: 8px;"><input type="radio" name="weather-unit" id="weather-unit-f" value="f"> Fahrenheit (°F)</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>System Information</h3>
|
||||
<div class="debug-info" id="debug-info">Loading debug info...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Appearance Panel -->
|
||||
<section class="tab-panel" id="panel-appearance" role="tabpanel" aria-labelledby="tab-appearance">
|
||||
<h2>Appearance</h2>
|
||||
<!-- Theme Selection -->
|
||||
<div class="customization-group">
|
||||
<h3>Theme Presets</h3>
|
||||
<div class="theme-selector">
|
||||
<button id="theme-default" class="theme-btn" data-theme="default">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #121418, #1B1035);"></div>
|
||||
<span>Default</span>
|
||||
</button>
|
||||
<button id="theme-ocean" class="theme-btn" data-theme="ocean">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1a365d, #2c5282);"></div>
|
||||
<span>Ocean</span>
|
||||
</button>
|
||||
<button id="theme-forest" class="theme-btn" data-theme="forest">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1a202c, #2d3748);"></div>
|
||||
<span>Forest</span>
|
||||
</button>
|
||||
<button id="theme-sunset" class="theme-btn" data-theme="sunset">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #744210, #c05621);"></div>
|
||||
<span>Sunset</span>
|
||||
</button>
|
||||
<button id="theme-cyberpunk" class="theme-btn" data-theme="cyberpunk">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0a0a0a, #2a0a3a, #1a0520);"></div>
|
||||
<span>Cyberpunk</span>
|
||||
</button>
|
||||
<button id="theme-midnight-rose" class="theme-btn" data-theme="midnight-rose">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #1c1820, #3d3046);"></div>
|
||||
<span>Midnight Rose</span>
|
||||
</button>
|
||||
<button id="theme-arctic-ice" class="theme-btn" data-theme="arctic-ice">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #f0f8ff, #d1e7ff);"></div>
|
||||
<span>Arctic Ice</span>
|
||||
</button>
|
||||
<button id="theme-cherry-blossom" class="theme-btn" data-theme="cherry-blossom">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #fff5f8, #ffd4db);"></div>
|
||||
<span>Cherry Blossom</span>
|
||||
</button>
|
||||
<button id="theme-cosmic-purple" class="theme-btn" data-theme="cosmic-purple">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0f0524, #2d1b69, #4b0082);"></div>
|
||||
<span>Cosmic Purple</span>
|
||||
</button>
|
||||
<button id="theme-emerald-dream" class="theme-btn" data-theme="emerald-dream">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #0d2818, #2d5a44);"></div>
|
||||
<span>Emerald Dream</span>
|
||||
</button>
|
||||
<button id="theme-mocha-coffee" class="theme-btn" data-theme="mocha-coffee">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #3c2414, #5d3a26);"></div>
|
||||
<span>Mocha Coffee</span>
|
||||
</button>
|
||||
<button id="theme-lavender-fields" class="theme-btn" data-theme="lavender-fields">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, #f8f4ff, #e6d8ff);"></div>
|
||||
<span>Lavender Fields</span>
|
||||
</button>
|
||||
<button id="theme-custom" class="theme-btn custom-theme-btn" data-theme="custom" style="display: none;">
|
||||
<div class="theme-preview" style="background: linear-gradient(145deg, var(--bg), var(--gradient-color));"></div>
|
||||
<span>Custom</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Scale -->
|
||||
<div class="customization-group">
|
||||
<h3>Display Scale</h3>
|
||||
<p class="note">Adjust the zoom level for this window. Changes apply immediately.</p>
|
||||
<div class="zoom-controls">
|
||||
<button class="zoom-btn" id="zoom-decrease" title="Decrease zoom">−</button>
|
||||
<span id="display-scale-value" class="zoom-value">100%</span>
|
||||
<button class="zoom-btn" id="zoom-increase" title="Increase zoom">+</button>
|
||||
</div>
|
||||
<div class="zoom-presets">
|
||||
<button class="zoom-preset-btn" data-zoom="60">60%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="70">70%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="80">80%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="90">90%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="100">100%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="110">110%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="120">120%</button>
|
||||
<button class="zoom-preset-btn" data-zoom="130">130%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Customization -->
|
||||
<div class="customization-group">
|
||||
<h3>Custom Colors</h3>
|
||||
<div class="color-controls">
|
||||
<div class="color-group">
|
||||
<label for="bg-color">Background:</label>
|
||||
<input type="color" id="bg-color" value="#121418">
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label for="gradient-color">Gradient End:</label>
|
||||
<input type="color" id="gradient-color" value="#1B1035">
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label for="accent-color">Accent:</label>
|
||||
<input type="color" id="accent-color" value="#7B2EFF">
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label for="secondary-color">Secondary:</label>
|
||||
<input type="color" id="secondary-color" value="#00C6FF">
|
||||
</div>
|
||||
<div class="color-group">
|
||||
<label for="text-color">Text:</label>
|
||||
<input type="color" id="text-color" value="#E0E0E0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Logo Customization -->
|
||||
<div class="customization-group">
|
||||
<h3>Logo & Branding</h3>
|
||||
<div class="logo-options">
|
||||
<label for="show-logo">
|
||||
<input type="checkbox" id="show-logo" checked>
|
||||
Show Nebula Logo
|
||||
</label>
|
||||
<label for="custom-title">
|
||||
Custom Title:
|
||||
<input type="text" id="custom-title" placeholder="Nebula Browser" maxlength="50">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Management -->
|
||||
<div class="customization-group">
|
||||
<h3>Theme Management</h3>
|
||||
<div class="theme-management">
|
||||
<button id="save-custom-theme">Save Current as Custom Theme</button>
|
||||
<button id="export-theme">Export Theme</button>
|
||||
<button id="import-theme">Import Theme</button>
|
||||
<input type="file" id="theme-file-input" accept=".json" style="display: none;">
|
||||
<button id="reset-to-default">Reset to Default</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="customization-group">
|
||||
<h3>Preview</h3>
|
||||
<div class="preview-container" id="preview-container">
|
||||
<div class="preview-home">
|
||||
<div class="preview-logo" id="preview-logo">../assets/images/branding/Nebula-Logo.svg</div>
|
||||
<div class="preview-text">Nebula</div>
|
||||
<div class="preview-search"></div>
|
||||
<div class="preview-bookmarks">
|
||||
<div class="preview-bookmark"></div>
|
||||
<div class="preview-bookmark"></div>
|
||||
<div class="preview-bookmark"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- History Panel -->
|
||||
<section class="tab-panel" id="panel-history" role="tabpanel" aria-labelledby="tab-history">
|
||||
<h2>History</h2>
|
||||
<div class="customization-group">
|
||||
<h3>Search History</h3>
|
||||
<ul id="search-history-list"></ul>
|
||||
<button id="clear-search-history-btn" style="margin-top: 10px;">Clear Search History</button>
|
||||
</div>
|
||||
<div class="customization-group">
|
||||
<h3>Site History</h3>
|
||||
<ul id="site-history-list"></ul>
|
||||
<div class="button-row" style="margin-top:10px;">
|
||||
<button id="clear-site-history-btn">Clear Site History</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Plugins Panel -->
|
||||
<section class="tab-panel" id="panel-plugins" role="tabpanel" aria-labelledby="tab-plugins">
|
||||
<h2>Plugins</h2>
|
||||
<div class="customization-group">
|
||||
<div class="button-row">
|
||||
<button id="plugins-reload-all">Reload Plugins</button>
|
||||
<span class="note">Changes to renderer preloads may require app restart.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customization-group">
|
||||
<h3>Installed</h3>
|
||||
<div id="plugins-list" class="plugins-list" role="list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About Panel -->
|
||||
<section class="tab-panel" id="panel-about" role="tabpanel" aria-labelledby="tab-about">
|
||||
<h2>About</h2>
|
||||
<div class="customization-group">
|
||||
<h3>Application</h3>
|
||||
<ul id="about-app">
|
||||
<li><strong>Name:</strong> <span id="about-app-name">Loading...</span></li>
|
||||
<li><strong>Version:</strong> <span id="about-app-version">Loading...</span></li>
|
||||
<li><strong>Packaged:</strong> <span id="about-packaged">Loading...</span></li>
|
||||
<li><strong>User data:</strong> <span id="about-userdata" style="word-break: break-all;">Loading...</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="customization-group">
|
||||
<h3>Runtime</h3>
|
||||
<ul id="about-runtime">
|
||||
<li><strong>CEF:</strong> <span id="about-cef">Chromium Embedded Framework</span></li>
|
||||
<li><strong>Chromium:</strong> <span id="about-chrome">Loading...</span></li>
|
||||
<li><strong>Node.js:</strong> <span id="about-node">Loading...</span></li>
|
||||
<li><strong>V8:</strong> <span id="about-v8">Loading...</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="customization-group">
|
||||
<h3>System</h3>
|
||||
<ul id="about-system">
|
||||
<li><strong>OS:</strong> <span id="about-os">Loading...</span></li>
|
||||
<li><strong>CPU:</strong> <span id="about-cpu">Loading...</span></li>
|
||||
<li><strong>Architecture:</strong> <span id="about-arch">Loading...</span></li>
|
||||
<li><strong>Memory:</strong> <span id="about-mem">Loading...</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="customization-group">
|
||||
<h3>Runtime Updates</h3>
|
||||
<p class="note">Nebula Browser now runs on CEF. Runtime updates are handled by the native application build.</p>
|
||||
<p><strong>Nebula Browser:</strong> <span id="about-app-version-copy">Loading...</span></p>
|
||||
</div>
|
||||
|
||||
<div class="customization-group about-actions">
|
||||
<button id="copy-about-btn">Copy diagnostics</button>
|
||||
<a id="github-link" href="https://github.com/Bobbybear007/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
|
||||
<!-- GitHub mark (Octicons) MIT License -->
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" role="img">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.01.08-2.11 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.91.08 2.11.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
|
||||
<!-- Help icon -->
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" role="img">
|
||||
<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 15a1.25 1.25 0 1 1 0 2.5A1.25 1.25 0 0 1 12 17zm-.02-1.9c-.6 0-1.02-.42-1.02-.98 0-1.97 2.76-1.95 2.76-3.62 0-.77-.66-1.4-1.72-1.4-1 0-1.67.5-2.06 1.22-.3.54-.95.73-1.45.44-.54-.31-.72-1-.42-1.53C7.7 7.7 9.06 6.5 12 6.5c2.26 0 3.98 1.3 3.98 3.35 0 2.74-2.96 2.77-3 3.83-.03.6-.43 1.02-1 1.02z"/>
|
||||
</svg>
|
||||
<span>Help</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</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="../js/settings.js"></script>
|
||||
<script src="../js/customization.js"></script>
|
||||
<script>
|
||||
// Apply saved theme immediately when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
BrowserCustomizer.applyThemeToPage();
|
||||
});
|
||||
|
||||
// Update debug info
|
||||
function updateDebugInfo() {
|
||||
const debugDiv = document.getElementById('debug-info');
|
||||
const localStorage = window.localStorage;
|
||||
const hasNativeBridge = !!window.electronAPI;
|
||||
const siteHistory = localStorage ? localStorage.getItem('siteHistory') : 'N/A';
|
||||
|
||||
debugDiv.innerHTML = `
|
||||
localStorage available: ${!!localStorage}<br>
|
||||
native bridge available: ${hasNativeBridge}<br>
|
||||
siteHistory in localStorage: ${siteHistory || 'null'}<br>
|
||||
Current domain: ${window.location.hostname}<br>
|
||||
Current protocol: ${window.location.protocol}
|
||||
`;
|
||||
}
|
||||
|
||||
// Get site history from localStorage in this webview context
|
||||
function getSiteHistoryFromLocalStorage() {
|
||||
try {
|
||||
const history = localStorage.getItem('siteHistory');
|
||||
console.log('[SETTINGS DEBUG] localStorage siteHistory:', history);
|
||||
return history ? JSON.parse(history) : [];
|
||||
} catch (err) {
|
||||
console.error('[SETTINGS DEBUG] Error reading from localStorage:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get search history from localStorage in this webview context
|
||||
function getSearchHistoryFromLocalStorage() {
|
||||
try {
|
||||
const history = localStorage.getItem('searchHistory');
|
||||
console.log('[SETTINGS DEBUG] localStorage searchHistory:', history);
|
||||
return history ? JSON.parse(history) : [];
|
||||
} catch (err) {
|
||||
console.error('[SETTINGS DEBUG] Error reading search history from localStorage:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Sync site history from main browser's localStorage to this webview's localStorage
|
||||
function syncSiteHistoryFromMain() {
|
||||
try {
|
||||
// Try to get the parent window's localStorage
|
||||
if (window.parent && window.parent !== window) {
|
||||
const parentHistory = window.parent.localStorage.getItem('siteHistory');
|
||||
if (parentHistory) {
|
||||
localStorage.setItem('siteHistory', parentHistory);
|
||||
console.log('[SETTINGS DEBUG] Synced history from parent window');
|
||||
return JSON.parse(parentHistory);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
} catch (err) {
|
||||
console.log('[SETTINGS DEBUG] Could not sync from parent:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Sync search history from main browser's localStorage to this webview's localStorage
|
||||
function syncSearchHistoryFromMain() {
|
||||
try {
|
||||
// Try to get the parent window's localStorage
|
||||
if (window.parent && window.parent !== window) {
|
||||
const parentHistory = window.parent.localStorage.getItem('searchHistory');
|
||||
if (parentHistory) {
|
||||
localStorage.setItem('searchHistory', parentHistory);
|
||||
console.log('[SETTINGS DEBUG] Synced search history from parent window');
|
||||
return JSON.parse(parentHistory);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
} catch (err) {
|
||||
console.log('[SETTINGS DEBUG] Could not sync search history from parent:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addTestHistory() {
|
||||
const testSites = [
|
||||
'https://www.google.com',
|
||||
'https://github.com',
|
||||
'https://stackoverflow.com',
|
||||
'https://developer.mozilla.org',
|
||||
'https://www.wikipedia.org'
|
||||
];
|
||||
|
||||
testSites.forEach(site => {
|
||||
try {
|
||||
let history = getSiteHistoryFromLocalStorage();
|
||||
history = history.filter(item => item !== site);
|
||||
history.unshift(site);
|
||||
localStorage.setItem('siteHistory', JSON.stringify(history));
|
||||
console.log('[SETTINGS DEBUG] Added test site:', site);
|
||||
} catch (err) {
|
||||
console.error('Error adding test history:', err);
|
||||
}
|
||||
});
|
||||
|
||||
loadHistories();
|
||||
}
|
||||
|
||||
async function loadHistories() {
|
||||
try {
|
||||
console.log('[SETTINGS DEBUG] Loading histories...');
|
||||
updateDebugInfo();
|
||||
|
||||
// First try to sync from parent window
|
||||
let siteHistory = syncSiteHistoryFromMain();
|
||||
|
||||
// If that didn't work, get from local storage
|
||||
if (siteHistory.length === 0) {
|
||||
siteHistory = getSiteHistoryFromLocalStorage();
|
||||
}
|
||||
|
||||
// Try to get search history from parent or localStorage
|
||||
let searchHistory = syncSearchHistoryFromMain();
|
||||
if (searchHistory.length === 0) {
|
||||
searchHistory = getSearchHistoryFromLocalStorage();
|
||||
}
|
||||
|
||||
const searchList = document.getElementById('search-history-list');
|
||||
const siteList = document.getElementById('site-history-list');
|
||||
|
||||
// Clear existing content
|
||||
searchList.innerHTML = '';
|
||||
siteList.innerHTML = '';
|
||||
|
||||
// Populate search history
|
||||
if (searchHistory && searchHistory.length > 0) {
|
||||
console.log('[SETTINGS DEBUG] Displaying', searchHistory.length, 'search history items');
|
||||
searchHistory.forEach(query => {
|
||||
const li = document.createElement('li');
|
||||
li.style.wordBreak = 'break-all';
|
||||
li.textContent = query;
|
||||
li.style.color = 'var(--text)';
|
||||
searchList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
const searchLi = document.createElement('li');
|
||||
searchLi.textContent = 'No search history yet';
|
||||
searchLi.style.fontStyle = 'italic';
|
||||
searchLi.style.color = '#666';
|
||||
searchList.appendChild(searchLi);
|
||||
}
|
||||
|
||||
// Populate site history
|
||||
if (siteHistory && siteHistory.length > 0) {
|
||||
console.log('[SETTINGS DEBUG] Displaying', siteHistory.length, 'site history items');
|
||||
siteHistory.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.style.wordBreak = 'break-all';
|
||||
// Create a clickable link that asks host to navigate in a new tab
|
||||
const a = document.createElement('a');
|
||||
a.href = item;
|
||||
a.textContent = item;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener noreferrer';
|
||||
a.style.color = 'var(--accent)';
|
||||
a.style.textDecoration = 'none';
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
|
||||
// Ask the host to open this URL in a new tab to keep Settings open
|
||||
window.electronAPI.sendToHost('navigate', item, { newTab: true });
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
// Fallback: postMessage to parent if available
|
||||
window.parent.postMessage({ type: 'navigate', url: item }, '*');
|
||||
} else {
|
||||
// Last resort: open in this webview
|
||||
window.location.href = item;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SETTINGS DEBUG] Failed to trigger navigation for', item, err);
|
||||
}
|
||||
});
|
||||
li.appendChild(a);
|
||||
siteList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
console.log('[SETTINGS DEBUG] No site history to display');
|
||||
const li = document.createElement('li');
|
||||
li.textContent = 'No browsing history found. Browse some websites first!';
|
||||
li.style.fontStyle = 'italic';
|
||||
li.style.color = '#666';
|
||||
siteList.appendChild(li);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error('[SETTINGS DEBUG] Error loading histories:', err);
|
||||
|
||||
const searchList = document.getElementById('search-history-list');
|
||||
const siteList = document.getElementById('site-history-list');
|
||||
|
||||
const errorLi1 = document.createElement('li');
|
||||
errorLi1.textContent = 'Error loading search history: ' + err.message;
|
||||
errorLi1.style.color = 'red';
|
||||
searchList.appendChild(errorLi1);
|
||||
|
||||
const errorLi2 = document.createElement('li');
|
||||
errorLi2.textContent = 'Error loading site history: ' + err.message;
|
||||
errorLi2.style.color = 'red';
|
||||
siteList.appendChild(errorLi2);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearSiteHistory() {
|
||||
try {
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem('siteHistory');
|
||||
console.log('[SETTINGS DEBUG] Cleared site history from localStorage');
|
||||
|
||||
loadHistories(); // Refresh the display
|
||||
} catch (err) {
|
||||
console.error('Error clearing site history:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearSearchHistory() {
|
||||
try {
|
||||
// Clear from localStorage
|
||||
localStorage.removeItem('searchHistory');
|
||||
console.log('[SETTINGS DEBUG] Cleared search history from localStorage');
|
||||
|
||||
// Also clear in parent window if accessible
|
||||
if (window.parent && window.parent !== window) {
|
||||
try {
|
||||
window.parent.localStorage.removeItem('searchHistory');
|
||||
console.log('[SETTINGS DEBUG] Cleared search history from parent');
|
||||
} catch (e) {
|
||||
console.log('[SETTINGS DEBUG] Could not clear parent search history:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
loadHistories(); // Refresh the display
|
||||
} catch (err) {
|
||||
console.error('Error clearing search history:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load histories on page load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('[SETTINGS DEBUG] DOM loaded, window.electronAPI:', !!window.electronAPI);
|
||||
loadHistories();
|
||||
|
||||
// Refresh history every few seconds to pick up new browsing
|
||||
setInterval(loadHistories, 3000);
|
||||
|
||||
// Add test history button functionality
|
||||
const addTestBtn = document.getElementById('add-test-history-btn');
|
||||
if (addTestBtn) {
|
||||
addTestBtn.addEventListener('click', addTestHistory);
|
||||
}
|
||||
|
||||
// Add clear buttons functionality
|
||||
const clearSiteBtn = document.getElementById('clear-site-history-btn');
|
||||
const clearSearchBtn = document.getElementById('clear-search-history-btn');
|
||||
|
||||
if (clearSiteBtn) {
|
||||
clearSiteBtn.addEventListener('click', clearSiteHistory);
|
||||
}
|
||||
if (clearSearchBtn) {
|
||||
clearSearchBtn.addEventListener('click', clearSearchHistory);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Nebula</title>
|
||||
<link rel="stylesheet" href="../css/setup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="setup-container">
|
||||
<!-- Progress indicator -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-step active" data-step="1">
|
||||
<div class="step-circle">1</div>
|
||||
<div class="step-label">Welcome</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="2">
|
||||
<div class="step-circle">2</div>
|
||||
<div class="step-label">Theme</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="3">
|
||||
<div class="step-circle">3</div>
|
||||
<div class="step-label">Default Browser</div>
|
||||
</div>
|
||||
<div class="progress-line"></div>
|
||||
<div class="progress-step" data-step="4">
|
||||
<div class="step-circle">4</div>
|
||||
<div class="step-label">Complete</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div class="setup-step active" data-step="1">
|
||||
<div class="step-content">
|
||||
|
||||
<h1 class="setup-title">Welcome to Nebula</h1>
|
||||
<p class="setup-subtitle">Let's personalize your browsing experience</p>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>Beautiful Themes</h3>
|
||||
<p>Choose from stunning themes or create your own</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🚀</div>
|
||||
<h3>Lightning Fast</h3>
|
||||
<p>Built for speed and performance</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🎮</div>
|
||||
<h3>Steam Deck Ready</h3>
|
||||
<p>Optimized for gaming handhelds</p>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>Privacy First</h3>
|
||||
<p>Your data stays yours</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-primary" id="btn-start">Get Started</button>
|
||||
<button class="btn btn-secondary" id="btn-skip-all">Skip Setup</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Theme Selection -->
|
||||
<div class="setup-step" data-step="2">
|
||||
<div class="step-content">
|
||||
<h1 class="setup-title">Choose Your Theme</h1>
|
||||
<p class="setup-subtitle">Pick a color scheme that suits your style</p>
|
||||
<div class="theme-grid" id="theme-grid">
|
||||
<!-- Themes will be dynamically loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" id="btn-back-2">Back</button>
|
||||
<button class="btn btn-primary" id="btn-next-2">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Default Browser -->
|
||||
<div class="setup-step" data-step="3">
|
||||
<div class="step-content">
|
||||
<h1 class="setup-title">Set as Default Browser</h1>
|
||||
<p class="setup-subtitle">Make Nebula your go-to browser for all links</p>
|
||||
<div class="default-browser-section">
|
||||
<div class="default-browser-card">
|
||||
<div class="browser-icon">🌐</div>
|
||||
<h3>Quick Access</h3>
|
||||
<p>Open all web links automatically with Nebula</p>
|
||||
</div>
|
||||
<div class="default-browser-status" id="default-status">
|
||||
<div class="status-icon">⏳</div>
|
||||
<p class="status-text">Checking default browser status...</p>
|
||||
</div>
|
||||
<div class="default-browser-actions">
|
||||
<button class="btn btn-large btn-primary" id="btn-set-default">
|
||||
<span class="btn-icon">✓</span>
|
||||
Set as Default Browser
|
||||
</button>
|
||||
<p class="help-text">You can always change this later in settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-secondary" id="btn-back-3">Back</button>
|
||||
<button class="btn btn-primary" id="btn-skip-3">Skip for Now</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Complete -->
|
||||
<div class="setup-step" data-step="4">
|
||||
<div class="step-content">
|
||||
<div class="success-icon">✓</div>
|
||||
<h1 class="setup-title">All Set!</h1>
|
||||
<p class="setup-subtitle">You're ready to explore the web with Nebula</p>
|
||||
<div class="completion-summary" id="completion-summary">
|
||||
<!-- Summary will be populated dynamically -->
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="step-actions">
|
||||
<button class="btn btn-primary btn-large" id="btn-finish">Start Browsing</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/setup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||