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:
2025-09-06 21:15:57 +12:00
parent a880a4ff71
commit 285fc44124
7 changed files with 280 additions and 14 deletions
+53 -5
View File
@@ -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
View File
@@ -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">
+162
View File
@@ -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 icongrid, 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}&current=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 OpenMeteo 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();
}
});
+11
View File
@@ -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>
+18
View File
@@ -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