Add greeting, weather, and clock widgets to home page
Introduces a dynamic greeting, live clock, and weather widget to the home page. Adds a 'Reset' button for bookmarks, refines the bookmarks card UI, and updates CSS for new widgets and improved layout. Settings page now includes a weather unit preference (auto, Celsius, Fahrenheit) that syncs with the home page weather display.
This commit is contained in:
+53
-5
@@ -39,11 +39,21 @@ body, html {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center; /* Center content vertically */
|
||||
height: 100vh;
|
||||
justify-content: flex-start;
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
padding: 4rem 2rem 2rem;
|
||||
}
|
||||
|
||||
/* Greeting hero title */
|
||||
.greeting-title {
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
color: #cfd4ff;
|
||||
text-shadow: 0 4px 22px rgba(0,0,0,0.6);
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +83,7 @@ body, html {
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
width: 550px; /* Increased width for the new button */
|
||||
max-width: 95vw;
|
||||
}
|
||||
@@ -195,6 +205,27 @@ body, html {
|
||||
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: radial-gradient(120% 140% at 0% 0%, rgba(255,255,255,0.06), rgba(255,255,255,0.03) 45%, rgba(255,255,255,0.02));
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
box-shadow: 0 18px 50px -20px rgba(0,0,0,0.6), inset 0 1px 0 rgba(255,255,255,0.06);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.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: #dfe3ff; opacity: .9; }
|
||||
.link-btn {
|
||||
background: none; border: none; color: #9aa8ff; cursor: pointer; font-size: .9rem;
|
||||
}
|
||||
.link-btn:hover { color: #c7d0ff; text-decoration: underline; }
|
||||
|
||||
/* Individual bookmark tile */
|
||||
.bookmark {
|
||||
background: rgba(255,255,255,0.05);
|
||||
@@ -309,7 +340,7 @@ body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
border-radius: 20px;
|
||||
font-size: 2rem;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px dashed rgba(255,255,255,0.3);
|
||||
@@ -423,6 +454,23 @@ body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
||||
background: #6a24e5;
|
||||
}
|
||||
|
||||
/* At a glance widget */
|
||||
.glance { position: fixed; right: 22px; bottom: 22px; }
|
||||
.glance-card {
|
||||
min-width: 280px; background: rgba(12,16,26,0.55); border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 16px; padding: 1rem; box-shadow: 0 14px 40px -18px rgba(0,0,0,.8), inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.glance-title { font-size: .95rem; color: #dfe3ff; opacity: .9; margin-bottom: .65rem; }
|
||||
.glance-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
|
||||
.glance-tile { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: .6rem .75rem; text-align:left; }
|
||||
.glance-label { font-size: .7rem; color: #b8c1ff; opacity: .85; margin-bottom: .25rem; }
|
||||
.glance-value { font-size: 1.05rem; letter-spacing: .3px; color:#fff; }
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.glance { position: static; margin-top: 1rem; }
|
||||
}
|
||||
|
||||
/* Color Palette */
|
||||
:root {
|
||||
--bg: #121418;
|
||||
|
||||
+32
-4
@@ -12,7 +12,11 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="home-container">
|
||||
<div class="logo">
|
||||
<!-- 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/Logos/Nebula-Logo.svg" class="logo-img">
|
||||
<div class="logo-text">Nebula Browser</div>
|
||||
</div>
|
||||
@@ -42,11 +46,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bookmarks" id="bookmarkList">
|
||||
<!-- Bookmarks dynamically inserted here -->
|
||||
</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>
|
||||
|
||||
<!-- Popup for adding a bookmark -->
|
||||
<div id="addPopup" class="popup hidden">
|
||||
<div class="popup-inner">
|
||||
|
||||
@@ -17,6 +17,10 @@ const iconGrid = document.getElementById('iconGrid');
|
||||
const selectedIconInput= document.getElementById('selectedIcon');
|
||||
const iconCategoryNav = document.getElementById('iconCategoryNav');
|
||||
const useFaviconCheckbox = document.getElementById('useFavicon');
|
||||
const greetingEl = document.getElementById('greeting');
|
||||
const resetTopSitesBtn = document.getElementById('resetTopSites');
|
||||
const clockEl = document.getElementById('clock');
|
||||
const weatherEl = document.getElementById('weather');
|
||||
let selectedIcon = initialIcons[0];
|
||||
let availableIcons = initialIcons;
|
||||
let currentIconSetKey = 'material';
|
||||
@@ -150,6 +154,19 @@ function renderBookmarks() {
|
||||
bookmarkList.appendChild(addBox);
|
||||
}
|
||||
|
||||
// Reset Top Sites (bookmarks) to empty state
|
||||
if (resetTopSitesBtn) {
|
||||
resetTopSitesBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!bookmarks.length) return;
|
||||
const yes = confirm('Clear all Top Sites?');
|
||||
if (!yes) return;
|
||||
bookmarks = [];
|
||||
await saveBookmarks();
|
||||
renderBookmarks();
|
||||
});
|
||||
}
|
||||
|
||||
// draw the icon‐grid, filtering by the search term
|
||||
function renderIconGrid(filter = '') {
|
||||
const f = filter.toLowerCase();
|
||||
@@ -516,3 +533,148 @@ function setupSectionObserver() {
|
||||
renderBookmarks();
|
||||
}, 100);
|
||||
})();
|
||||
|
||||
// ---- Greeting / Clock / Weather widgets ----
|
||||
function computeGreeting(d = new Date()) {
|
||||
const h = d.getHours();
|
||||
if (h < 5) return 'Good Night';
|
||||
if (h < 12) return 'Good Morning';
|
||||
if (h < 18) return 'Good Afternoon';
|
||||
return 'Good Evening';
|
||||
}
|
||||
|
||||
function startClock() {
|
||||
const tick = () => {
|
||||
const now = new Date();
|
||||
if (greetingEl) greetingEl.textContent = computeGreeting(now);
|
||||
if (clockEl) clockEl.textContent = now.toLocaleTimeString([], { hour12: true });
|
||||
};
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
}
|
||||
|
||||
// Unit helpers
|
||||
const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f'
|
||||
const COUNTRIES_FAHRENHEIT = new Set(['US','BS','KY','LR','PW','FM','MH']);
|
||||
function useFahrenheit() {
|
||||
try {
|
||||
const pref = localStorage.getItem(WEATHER_UNIT_KEY);
|
||||
if (pref === 'c') return false; if (pref === 'f') return true;
|
||||
} catch {}
|
||||
try {
|
||||
const loc = Intl.DateTimeFormat().resolvedOptions().locale || navigator.language || '';
|
||||
const region = loc.split('-')[1];
|
||||
return region ? COUNTRIES_FAHRENHEIT.has(region.toUpperCase()) : false;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function getPosition(timeoutMs = 6000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!('geolocation' in navigator)) return reject(new Error('geolocation unavailable'));
|
||||
const opts = { enableHighAccuracy: false, timeout: timeoutMs, maximumAge: 60_000 };
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
|
||||
err => reject(err),
|
||||
opts
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function geoByIP() {
|
||||
// Try a couple of CORS-friendly IP services
|
||||
try {
|
||||
const r = await fetch('https://ipapi.co/json/');
|
||||
if (r.ok) {
|
||||
const j = await r.json();
|
||||
if (j && typeof j.latitude === 'number' && typeof j.longitude === 'number') {
|
||||
return { lat: j.latitude, lon: j.longitude, city: j.city, country: j.country_code };
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const r = await fetch('https://ipwho.is/');
|
||||
if (r.ok) {
|
||||
const j = await r.json();
|
||||
if (j && j.success && j.latitude && j.longitude) {
|
||||
return { lat: j.latitude, lon: j.longitude, city: j.city, country: j.country_code };
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchOpenMeteo(lat, lon, fahrenheit) {
|
||||
const tUnit = fahrenheit ? 'fahrenheit' : 'celsius';
|
||||
const wUnit = fahrenheit ? 'mph' : 'kmh';
|
||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code,wind_speed_10m&temperature_unit=${tUnit}&windspeed_unit=${wUnit}&timezone=auto`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error('weather fetch failed');
|
||||
const j = await r.json();
|
||||
return {
|
||||
temp: j?.current?.temperature_2m,
|
||||
wind: j?.current?.wind_speed_10m,
|
||||
code: j?.current?.weather_code,
|
||||
tUnit: fahrenheit ? '°F' : '°C',
|
||||
wUnit: fahrenheit ? 'mph' : 'km/h',
|
||||
};
|
||||
}
|
||||
|
||||
function codeToSummary(code) {
|
||||
// Minimal Open‑Meteo WMO code mapping
|
||||
const m = new Map([
|
||||
[0,'Clear'], [1,'Mainly clear'], [2,'Partly cloudy'], [3,'Cloudy'],
|
||||
[45,'Fog'], [48,'Rime fog'], [51,'Drizzle'], [53,'Drizzle'], [55,'Drizzle'],
|
||||
[56,'Freezing drizzle'], [57,'Freezing drizzle'],
|
||||
[61,'Rain'], [63,'Rain'], [65,'Rain'],
|
||||
[66,'Freezing rain'], [67,'Freezing rain'],
|
||||
[71,'Snow'], [73,'Snow'], [75,'Snow'], [77,'Snow grains'],
|
||||
[80,'Showers'], [81,'Showers'], [82,'Heavy showers'],
|
||||
[85,'Snow showers'], [86,'Snow showers'],
|
||||
[95,'Thunderstorm'], [96,'Storm'], [99,'Severe storm']
|
||||
]);
|
||||
return m.get(Number(code)) || 'Weather';
|
||||
}
|
||||
|
||||
async function loadWeather() {
|
||||
if (!weatherEl) return;
|
||||
// Prefer an app-provided IPC source if available
|
||||
try {
|
||||
if (window.electronAPI && typeof window.electronAPI.invoke === 'function') {
|
||||
const res = await window.electronAPI.invoke('get-weather');
|
||||
if (res && (res.temp || res.summary)) {
|
||||
const summaryText = res.summary || '';
|
||||
const tempText = typeof res.temp === 'number' ? `${Math.round(res.temp)}°` : '';
|
||||
const windText = res.wind ? ` · Wind ${Math.round(res.wind)} ${res.wUnit || 'km/h'}` : '';
|
||||
weatherEl.textContent = `${tempText}${summaryText ? ' · ' + summaryText : ''}${windText}`.trim() || '—';
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('IPC weather failed', e); }
|
||||
|
||||
try {
|
||||
// 1) Try browser geolocation
|
||||
let loc = null;
|
||||
try { loc = await getPosition(); } catch {}
|
||||
if (!loc) loc = await geoByIP();
|
||||
if (!loc) throw new Error('no location');
|
||||
const f = useFahrenheit();
|
||||
const data = await fetchOpenMeteo(loc.lat, loc.lon, f);
|
||||
const summary = codeToSummary(data.code);
|
||||
const temp = typeof data.temp === 'number' ? Math.round(data.temp) : data.temp;
|
||||
const wind = typeof data.wind === 'number' ? Math.round(data.wind) : data.wind;
|
||||
weatherEl.textContent = `${temp}${data.tUnit} · Wind ${wind} ${data.wUnit}`;
|
||||
} catch (err) {
|
||||
console.warn('Weather fetch failed', err);
|
||||
weatherEl.textContent = '—';
|
||||
}
|
||||
}
|
||||
|
||||
startClock();
|
||||
loadWeather();
|
||||
|
||||
// Refresh weather when unit preference changes
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e && e.key === WEATHER_UNIT_KEY) {
|
||||
loadWeather();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -290,6 +290,17 @@
|
||||
<button id="clear-data-btn">Clear Data</button>
|
||||
</div>
|
||||
<p class="note">Settings are stored locally on this device.</p>
|
||||
|
||||
<div class="setting-group">
|
||||
<h3>Weather</h3>
|
||||
<fieldset class="weather-units" style="display:flex; flex-direction:column; gap:8px; border:1px solid rgba(255,255,255,0.15); padding:10px; border-radius:8px;">
|
||||
<legend style="padding:0 6px; opacity:.85;">Temperature units</legend>
|
||||
<label><input type="radio" name="weather-unit" id="weather-unit-auto" value="auto" checked> Auto (based on locale)</label>
|
||||
<label><input type="radio" name="weather-unit" id="weather-unit-c" value="c"> Celsius (°C)</label>
|
||||
<label><input type="radio" name="weather-unit" id="weather-unit-f" value="f"> Fahrenheit (°F)</label>
|
||||
</fieldset>
|
||||
<p class="note">Affects the weather card on the Home page.</p>
|
||||
</div>
|
||||
<div class="debug-info" id="debug-info">Loading debug info...</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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'
|
||||
|
||||
function showStatus(message) {
|
||||
if (statusText && statusDiv) {
|
||||
@@ -98,6 +99,23 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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); }
|
||||
});
|
||||
|
||||
// Tabs: simple controller
|
||||
|
||||
Reference in New Issue
Block a user