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:
Generated
+3
-3
@@ -1472,9 +1472,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-nightly": {
|
"node_modules/electron-nightly": {
|
||||||
"version": "39.0.0-nightly.20250811",
|
"version": "39.0.0-nightly.20250902",
|
||||||
"resolved": "https://registry.npmjs.org/electron-nightly/-/electron-nightly-39.0.0-nightly.20250811.tgz",
|
"resolved": "https://registry.npmjs.org/electron-nightly/-/electron-nightly-39.0.0-nightly.20250902.tgz",
|
||||||
"integrity": "sha512-WXL3vsoEZjtZCrgxgoBx7+pZYFz5LqKakO5ndkxnCZdDOGcHgPZ++DRVuoWIkL1yDW+ksIEJ/3kc/2gLN6llyQ==",
|
"integrity": "sha512-dWlw0mv/I1n70wXMSdT2rYzRsR1EBDvOAk9b4R4Wm4s4SVTVc1HtuF47iGZTqG2NW9eqi2FGhkraIOABxzjNjw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"run": "electron ."
|
"run": "electron ."
|
||||||
|
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
+53
-5
@@ -39,11 +39,21 @@ body, html {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center; /* Center content vertically */
|
justify-content: flex-start;
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
text-align: center;
|
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 {
|
.search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
width: 550px; /* Increased width for the new button */
|
width: 550px; /* Increased width for the new button */
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
}
|
}
|
||||||
@@ -195,6 +205,27 @@ body, html {
|
|||||||
max-width: 800px;
|
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 */
|
/* Individual bookmark tile */
|
||||||
.bookmark {
|
.bookmark {
|
||||||
background: rgba(255,255,255,0.05);
|
background: rgba(255,255,255,0.05);
|
||||||
@@ -309,7 +340,7 @@ body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 50%;
|
border-radius: 20px;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
background: rgba(255,255,255,0.05);
|
background: rgba(255,255,255,0.05);
|
||||||
border: 1px dashed rgba(255,255,255,0.3);
|
border: 1px dashed rgba(255,255,255,0.3);
|
||||||
@@ -423,6 +454,23 @@ body[data-theme="dark"] .bookmark .material-symbols-outlined,
|
|||||||
background: #6a24e5;
|
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 */
|
/* Color Palette */
|
||||||
:root {
|
:root {
|
||||||
--bg: #121418;
|
--bg: #121418;
|
||||||
|
|||||||
+29
-1
@@ -12,7 +12,11 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="home-container">
|
<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">
|
<img src="../assets/images/Logos/Nebula-Logo.svg" class="logo-img">
|
||||||
<div class="logo-text">Nebula Browser</div>
|
<div class="logo-text">Nebula Browser</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,11 +46,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="bookmarks" id="bookmarkList">
|
||||||
<!-- Bookmarks dynamically inserted here -->
|
<!-- Bookmarks dynamically inserted here -->
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</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 -->
|
<!-- Popup for adding a bookmark -->
|
||||||
<div id="addPopup" class="popup hidden">
|
<div id="addPopup" class="popup hidden">
|
||||||
<div class="popup-inner">
|
<div class="popup-inner">
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const iconGrid = document.getElementById('iconGrid');
|
|||||||
const selectedIconInput= document.getElementById('selectedIcon');
|
const selectedIconInput= document.getElementById('selectedIcon');
|
||||||
const iconCategoryNav = document.getElementById('iconCategoryNav');
|
const iconCategoryNav = document.getElementById('iconCategoryNav');
|
||||||
const useFaviconCheckbox = document.getElementById('useFavicon');
|
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 selectedIcon = initialIcons[0];
|
||||||
let availableIcons = initialIcons;
|
let availableIcons = initialIcons;
|
||||||
let currentIconSetKey = 'material';
|
let currentIconSetKey = 'material';
|
||||||
@@ -150,6 +154,19 @@ function renderBookmarks() {
|
|||||||
bookmarkList.appendChild(addBox);
|
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
|
// draw the icon‐grid, filtering by the search term
|
||||||
function renderIconGrid(filter = '') {
|
function renderIconGrid(filter = '') {
|
||||||
const f = filter.toLowerCase();
|
const f = filter.toLowerCase();
|
||||||
@@ -516,3 +533,148 @@ function setupSectionObserver() {
|
|||||||
renderBookmarks();
|
renderBookmarks();
|
||||||
}, 100);
|
}, 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>
|
<button id="clear-data-btn">Clear Data</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="note">Settings are stored locally on this device.</p>
|
<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>
|
<div class="debug-info" id="debug-info">Loading debug info...</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ let clearBtn = document.getElementById('clear-data-btn');
|
|||||||
const statusDiv = document.getElementById('status');
|
const statusDiv = document.getElementById('status');
|
||||||
const statusText = document.getElementById('status-text');
|
const statusText = document.getElementById('status-text');
|
||||||
const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
|
const TAB_STORAGE_KEY = 'nebula-settings-active-tab';
|
||||||
|
const WEATHER_UNIT_KEY = 'nebula-weather-unit'; // 'auto' | 'c' | 'f'
|
||||||
|
|
||||||
function showStatus(message) {
|
function showStatus(message) {
|
||||||
if (statusText && statusDiv) {
|
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
|
// Tabs: simple controller
|
||||||
|
|||||||
Reference in New Issue
Block a user