您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mostra o clima atual, previsão de 5 dias, tema escuro automático, cidade fixa se GPS falhar, botão de atualização e alternância C/F.
// ==UserScript== // @name BF - Clima Atual // @name:pt-BR BF - Clima Atual // @namespace https://github.com/BrunoFortunatto // @version 1.0 // @description Mostra o clima atual, previsão de 5 dias, tema escuro automático, cidade fixa se GPS falhar, botão de atualização e alternância C/F. // @description:pt-BR Mostra o clima atual, previsão de 5 dias, tema escuro automático, cidade fixa se GPS falhar, botão de atualização e alternância C/F. // @author Bruno Fortunato // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== /* * MIT License * * Copyright (c) 2025 Bruno Fortunato * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ (function () { 'use strict'; if (window.top !== window.self) return; // POR FAVOR, COLOQUE SUA CHAVE DE API DO OPENWEATHERMAP AQUI! // Você pode obter uma chave gratuita em: https://openweathermap.org/api const apiKey = 'SUA_CHAVE_API_AQUI'; // <-- Substitua 'SUA_CHAVE_API_AQUI' pela sua chave real if (apiKey === 'SUA_CHAVE_API_AQUI' || !apiKey) { const errorDiv = document.createElement('div'); errorDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffcccc; color: #cc0000; padding: 20px; border: 2px solid #cc0000; border-radius: 8px; font-family: sans-serif; font-size: 16px; text-align: center; z-index: 9999999; box-shadow: 0 4px 8px rgba(0,0,0,0.2); max-width: 80vw; `; errorDiv.innerHTML = ` <h3>Erro no Script BF - Clima Atual</h3> <p>Por favor, adicione sua chave de API do OpenWeatherMap para que o script funcione corretamente.</p> <p>Edite o script no seu gerenciador (Tampermonkey/Greasemonkey) e substitua 'SUA_CHAVE_API_AQUI' pela sua chave.</p> <p>Você pode obter uma chave gratuita em: <a href="https://openweathermap.org/api" target="_blank" rel="noopener noreferrer" style="color: #cc0000; text-decoration: underline;">openweathermap.org/api</a></p> <button style="margin-top: 15px; padding: 8px 15px; background: #cc0000; color: white; border: none; border-radius: 5px; cursor: pointer;">Entendi</button> `; document.body.appendChild(errorDiv); errorDiv.querySelector('button').addEventListener('click', () => errorDiv.remove()); return; } const fallbackCoords = { latitude: -30.0346, longitude: -51.2177 }; let coords = null; let usingFallbackCoords = false; let isCelsius = GM_getValue('weather_is_celsius', true); const CACHE_KEY_WEATHER_DATA = 'weather_data_cache'; const CACHE_KEY_LAST_UPDATE = 'weather_last_update'; const CACHE_KEY_COORDS_TYPE = 'weather_coords_type'; const CACHE_EXPIRATION_MS = 5 * 60 * 1000; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const colorPrimary = prefersDark ? '#333' : '#2196f3'; const colorText = prefersDark ? '#eee' : '#fff'; const colorPanel = prefersDark ? '#1c1c1c' : '#fff'; const colorTextPanel = prefersDark ? '#eee' : '#333'; const colorBorderLight = prefersDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; const tempMaxColor = prefersDark ? '#FF8C00' : '#FF6347'; const tempMinColor = prefersDark ? '#87CEEB' : '#4682B4'; const style = document.createElement('style'); style.textContent = ` .weather-btn { position: fixed; bottom: 20px; right: 20px; z-index: 999999; display: flex; align-items: center; gap: 8px; background: ${colorPrimary}; color: ${colorText}; padding: 8px 12px; border-radius: 10px; border: none; font-family: sans-serif; font-size: 14px; box-shadow: 0 6px 12px rgba(0,0,0,0.4); cursor: pointer; transition: background 0.2s, opacity 0.3s ease; opacity: 0.85; } .weather-btn:hover { background: #1976d2; } .weather-btn .weather-icon { width: 28px; height: 28px; vertical-align: middle; } .weather-panel { position: fixed; bottom: 20px; right: 20px; max-height: calc(100vh - 40px); overflow-y: auto; background: ${colorPanel}; color: ${colorTextPanel}; padding: 14px 18px; border-radius: 12px; box-shadow: 0 6px 12px rgba(0,0,0,0.4); font-family: sans-serif; font-size: 14px; max-width: 320px; width: 92vw; line-height: 1.5; display: none; animation: fadeIn 0.3s ease-in-out; z-index: 999999; box-sizing: border-box; } @media (max-width: 480px) { .weather-panel { right: 15px; left: 15px; max-width: calc(100vw - 30px); width: auto; bottom: 80px; } .weather-btn { right: 15px; bottom: 15px; } } .refresh-btn, .unit-toggle-btn { display: block; margin-top: 10px; padding: 6px 12px; border-radius: 8px; background: ${colorPrimary}; color: ${colorText}; cursor: pointer; transition: background 0.2s; font-size: 14px; width: 100%; box-sizing: border-box; border: none; } .refresh-btn:hover, .unit-toggle-btn:hover { background: #1976d2; } .forecast { margin-top: 12px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; border-top: 1px solid ${colorBorderLight}; padding-top: 15px; overflow-x: auto; } .forecast-day { text-align: center; font-size: 12px; padding: 5px 0; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-width: 60px; box-sizing: border-box; } .forecast-day strong { font-size: 12px; margin-bottom: 5px; opacity: 0.9; } .forecast-day img { width: 38px; height: 38px; margin-bottom: 5px; } .forecast-temp-max { font-weight: bold; color: ${tempMaxColor}; font-size: 13px; } .forecast-temp-min { opacity: 0.8; color: ${tempMinColor}; font-size: 12px; margin-right: 2px; } .forecast-temps { display: flex; align-items: center; justify-content: center; margin-top: 3px; } .forecast-rain-info { font-size: 10px; opacity: 0.8; margin-top: 2px; min-height: 15px; } .current-day-rain-info { font-size: 13px; font-weight: bold; color: ${prefersDark ? '#ADD8E6' : '#007bff'}; margin-top: 5px; } .weather-close-btn { position: absolute; top: 8px; right: 8px; background: none; border: none; color: ${colorTextPanel}; font-size: 18px; font-weight: bold; cursor: pointer; padding: 0 5px; line-height: 1; transition: color 0.2s; } .weather-close-btn:hover { color: #f00; } .full-forecast-link { display: block; margin-top: 10px; text-align: center; font-size: 13px; color: ${prefersDark ? '#ADD8E6' : '#007bff'}; text-decoration: underline; cursor: pointer; transition: color 0.2s; padding: 6px; border-top: 1px dashed ${colorBorderLight}; } .full-forecast-link:hover { color: ${prefersDark ? '#87CEEB' : '#0056b3'}; } .wind-info, .pressure-info, .humidity-info { font-size: 13px; font-weight: bold; margin-top: 5px; } .wind-info.calm { color: ${prefersDark ? '#98FB98' : '#32CD32'}; } .wind-info.light { color: ${prefersDark ? '#ADD8E6' : '#007bff'}; } .wind-info.moderate { color: ${prefersDark ? '#FFD700' : '#FFA500'}; } .wind-info.strong { color: ${prefersDark ? '#FF4500' : '#DC143C'}; } .wind-info.dangerous { color: ${prefersDark ? '#FF0000' : '#8B0000'}; } .pressure-info.high-pressure { color: ${prefersDark ? '#98FB98' : '#32CD32'}; } .pressure-info.normal-pressure { color: ${prefersDark ? '#ADD8E6' : '#007bff'}; } .pressure-info.low-pressure { color: ${prefersDark ? '#FFD700' : '#FFA500'}; } .pressure-info.very-low-pressure { color: ${prefersDark ? '#FF0000' : '#8B0000'}; } .pressure-info.unknown-pressure { color: ${prefersDark ? '#A9A9A9' : '#808080'}; } .humidity-info.very-dry { color: ${prefersDark ? '#FF6347' : '#DC143C'}; } .humidity-info.dry { color: ${prefersDark ? '#FFD700' : '#FFA500'}; } .humidity-info.comfortable { color: ${prefersDark ? '#98FB98' : '#32CD32'}; } .humidity-info.humid { color: ${prefersDark ? '#ADD8E6' : '#007bff'}; } .humidity-info.very-humid { color: ${prefersDark ? '#4682B4' : '#191970'}; } `; document.head.appendChild(style); const button = document.createElement('button'); button.className = 'weather-btn'; button.innerHTML = `<img class="weather-icon" src="https://openweathermap.org/img/wn/01d.png" alt="Carregando"> <span class="weather-text">Carregando...</span>`; const panel = document.createElement('div'); panel.className = 'weather-panel'; const refreshButton = document.createElement('button'); refreshButton.className = 'refresh-btn'; refreshButton.textContent = '🔄 Atualizar'; refreshButton.disabled = true; const unitToggleButton = document.createElement('button'); unitToggleButton.className = 'unit-toggle-btn'; unitToggleButton.textContent = `Mudar para ${isCelsius ? 'Fahrenheit' : 'Celsius'}`; const weatherDetailsContainer = document.createElement('div'); weatherDetailsContainer.className = 'weather-details-container'; const closeButton = document.createElement('button'); closeButton.className = 'weather-close-btn'; closeButton.innerHTML = '×'; closeButton.title = 'Fechar'; const fullForecastLink = document.createElement('a'); fullForecastLink.className = 'full-forecast-link'; fullForecastLink.textContent = 'Ver previsão completa no OpenWeatherMap'; fullForecastLink.target = '_blank'; fullForecastLink.rel = 'noopener noreferrer'; panel.append(closeButton, weatherDetailsContainer, refreshButton, unitToggleButton, fullForecastLink); document.body.append(button, panel); const closeWeatherPanel = () => panel.style.display = 'none'; button.addEventListener('click', () => panel.style.display = panel.style.display === 'block' ? 'none' : 'block'); closeButton.addEventListener('click', closeWeatherPanel); document.addEventListener('click', (event) => { if (panel.style.display === 'block' && !button.contains(event.target) && !panel.contains(event.target)) { closeWeatherPanel(); } }); const handleRefreshOrUnitToggle = async (isUnitToggle = false) => { if (!coords) { console.warn("Coordenadas não disponíveis, usando fallback."); coords = fallbackCoords; usingFallbackCoords = true; } refreshButton.textContent = '⏳ Atualizando...'; refreshButton.disabled = true; unitToggleButton.disabled = true; try { await updateWeather(coords.latitude, coords.longitude); } catch (error) { console.error(`Erro ao ${isUnitToggle ? 'alternar unidade' : 'atualizar'} clima:`, error); weatherDetailsContainer.innerHTML = ` <p style="color: red; text-align: center; padding: 10px;"> Não foi possível ${isUnitToggle ? 'alterar a unidade' : 'atualizar o clima'}. <br>Verifique sua conexão ou tente novamente mais tarde. <br>Detalhes: ${error.message} </p>`; panel.style.height = 'auto'; } finally { refreshButton.textContent = '🔄 Atualizar'; refreshButton.disabled = false; unitToggleButton.disabled = false; } }; refreshButton.addEventListener('click', handleRefreshOrUnitToggle); unitToggleButton.addEventListener('click', () => { isCelsius = !isCelsius; GM_setValue('weather_is_celsius', isCelsius); unitToggleButton.textContent = `Mudar para ${isCelsius ? 'Fahrenheit' : 'Celsius'}`; handleRefreshOrUnitToggle(true); }); const formatTime = (unix) => new Date(unix * 1000).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); const convertTemp = (temp) => isCelsius ? temp : (temp * 9/5) + 32; const getUnitSymbol = () => isCelsius ? '°C' : '°F'; const getWindDirectionCardinal = (degrees) => { const directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]; return directions[Math.round((degrees % 360) / 22.5)]; }; const getHumidityFeedback = (humidityPercent) => { if (humidityPercent < 30) return { description: 'Ar Muito Seco', className: 'very-dry' }; if (humidityPercent < 50) return { description: 'Ar Seco', className: 'dry' }; if (humidityPercent < 70) return { description: 'Umidade Confortável', className: 'comfortable' }; if (humidityPercent < 85) return { description: 'Ar Úmido', className: 'humid' }; return { description: 'Ar Muito Úmido/Saturado', className: 'very-humid' }; }; const getWindFeedback = (speedKmH) => { if (speedKmH < 1) return { description: 'Vento Calmo', className: 'calm' }; if (speedKmH < 6) return { description: 'Vento Muito Leve', className: 'light' }; if (speedKmH < 12) return { description: 'Vento Leve', className: 'light' }; if (speedKmH < 20) return { description: 'Vento Moderado', className: 'moderate' }; if (speedKmH < 29) return { description: 'Vento Forte', className: 'strong' }; if (speedKmH < 39) return { description: 'Vento Muito Forte', className: 'strong' }; if (speedKmH < 50) return { description: 'Vento Perigoso! Cuidado!', className: 'dangerous' }; return { description: 'Vento Tempestuoso! Alerta!', className: 'dangerous' }; }; const getPressureFeedback = (pressureHPa, humidity, clouds, hasRainToday) => { let description = 'Pressão: Dado indisponível ou incomum.'; let className = 'unknown-pressure'; if (pressureHPa >= 1022) { description = 'Pressão Alta: Tempo estável, céu claro.'; className = 'high-pressure'; if (humidity > 70 || clouds > 60) { description = 'Pressão Alta: Tempo estável, mas pode ter umidade ou nuvens.'; className = 'normal-pressure'; } } else if (pressureHPa >= 1013 && pressureHPa < 1022) { description = 'Pressão Normal: Tempo geralmente bom.'; className = 'normal-pressure'; if (hasRainToday || humidity > 75 || clouds > 70) { description = 'Pressão Normal: Possibilidade de tempo instável.'; className = 'low-pressure'; } } else if (pressureHPa >= 1005 && pressureHPa < 1013) { description = 'Pressão Baixa: Possibilidade de tempo instável e chuva.'; className = 'low-pressure'; if (hasRainToday || humidity > 80 || clouds > 80) { description = 'Pressão Baixa: Risco elevado de chuva e tempo adverso!'; className = 'very-low-pressure'; } } else if (pressureHPa < 1005) { description = 'Pressão Muito Baixa: Alto risco de chuva e tempestades!'; className = 'very-low-pressure'; if (!hasRainToday && humidity < 70 && clouds < 50) { description = 'Pressão Muito Baixa: Fique atento, tempo pode piorar.'; className = 'dangerous'; } } return { description, className }; }; async function fetchForecast(lat, lon) { try { const url = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric&lang=pt_br`; const res = await fetch(url); if (!res.ok) throw new Error(`Erro HTTP na previsão! Status: ${res.status}`); const data = await res.json(); const daily = {}; let currentDayRainPeriods = { morning: false, afternoon: false, night: false }; let hasRainToday = false; let firstDateString = null; data.list.forEach(item => { const date = item.dt_txt.split(' ')[0]; const weatherId = item.weather[0].id; const weatherIcon = item.weather[0].icon; const hour = new Date(item.dt * 1000).getHours(); if (!firstDateString) firstDateString = date; if (!daily[date]) { daily[date] = { min: item.main.temp_min, max: item.main.temp_max, icon: weatherIcon, hasRainOrSnow: false, rainPeriods: { morning: false, afternoon: false, night: false } }; } else { daily[date].min = Math.min(daily[date].min, item.main.temp_min); daily[date].max = Math.max(daily[date].max, item.main.temp_max); } const isRainingOrSnowing = (weatherId >= 200 && weatherId < 700); if (isRainingOrSnowing) { daily[date].hasRainOrSnow = true; if (date === firstDateString) hasRainToday = true; if (hour >= 6 && hour < 12) daily[date].rainPeriods.morning = true; else if (hour >= 12 && hour < 18) daily[date].rainPeriods.afternoon = true; else if (hour >= 18 || hour < 6) daily[date].rainPeriods.night = true; } }); if (firstDateString && daily[firstDateString]) currentDayRainPeriods = daily[firstDateString].rainPeriods; const forecastHtml = Object.entries(daily).slice(1, 6).map(([date, val]) => { const d = new Date(date + 'T12:00:00'); const dayOfWeek = d.toLocaleDateString('pt-BR', { weekday: 'short' }); let displayIcon = val.icon.endsWith('n') && !val.icon.startsWith('50') ? val.icon.replace('n', 'd') : val.icon; if (val.hasRainOrSnow && !['09', '10', '11'].some(id => displayIcon.startsWith(id)) && !displayIcon.startsWith('13')) displayIcon = '09d'; const periods = []; if (val.rainPeriods.morning) periods.push('manhã'); if (val.rainPeriods.afternoon) periods.push('tarde'); if (val.rainPeriods.night) periods.push('noite'); const rainMessage = periods.length > 0 ? `Prob. chuva ${periods.join(' e ')}` : 'Não deve chover'; return ` <div class="forecast-day"> <strong>${dayOfWeek.charAt(0).toUpperCase() + dayOfWeek.slice(1)}</strong> <img src="https://openweathermap.org/img/wn/${displayIcon}.png" alt="Ícone de clima"> <div class="forecast-temps"> <span class="forecast-temp-min">${convertTemp(val.min).toFixed(0)}${getUnitSymbol()}</span> <span class="forecast-temp-max">${convertTemp(val.max).toFixed(0)}${getUnitSymbol()}</span> </div> <div class="forecast-rain-info">${rainMessage}</div> </div>`; }).join(''); return { forecastHtml, currentDayRainPeriods, hasRainToday }; } catch (error) { console.error("Erro ao buscar previsão:", error); return { forecastHtml: '<div style="text-align: center; margin-top: 10px;">Não foi possível carregar a previsão.</div>', currentDayRainPeriods: { morning: false, afternoon: false, night: false }, hasRainToday: false }; } } function updateButtonDisplay(temp, icon, description) { const weatherIconElem = button.querySelector('.weather-icon'); const weatherTextElem = button.querySelector('.weather-text'); if (weatherIconElem && weatherTextElem) { let displayIcon = icon.endsWith('n') && !icon.startsWith('50') ? icon.replace('n', 'd') : icon; weatherIconElem.src = `https://openweathermap.org/img/wn/${displayIcon}.png`; weatherIconElem.alt = description; weatherTextElem.textContent = `${convertTemp(temp).toFixed(1)}${getUnitSymbol()}`; } } async function updateWeather(lat, lon, initialLoad = false) { try { const weatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${apiKey}&units=metric&lang=pt_br`; const weatherRes = await fetch(weatherUrl); if (!weatherRes.ok) throw new Error(`Erro HTTP: ${weatherRes.status}`); const weatherData = await weatherRes.json(); updateButtonDisplay(weatherData.main.temp, weatherData.weather[0].icon, weatherData.weather[0].description); if (typeof GM_setValue !== 'undefined') { GM_setValue(CACHE_KEY_WEATHER_DATA, JSON.stringify({ temp: weatherData.main.temp, icon: weatherData.weather[0].icon, description: weatherData.weather[0].description, latitude: lat, longitude: lon })); GM_setValue(CACHE_KEY_LAST_UPDATE, Date.now()); GM_setValue(CACHE_KEY_COORDS_TYPE, usingFallbackCoords ? 'fallback' : 'gps'); } const { forecastHtml, currentDayRainPeriods, hasRainToday } = await fetchForecast(lat, lon); const rainPeriods = []; if (currentDayRainPeriods.morning) rainPeriods.push('manhã'); if (currentDayRainPeriods.afternoon) rainPeriods.push('tarde'); if (currentDayRainPeriods.night) rainPeriods.push('noite'); const currentDayRainMessage = rainPeriods.length > 0 ? `Prob. de chuva ${rainPeriods.join(' e ')}` : 'Não deve chover hoje'; fullForecastLink.href = `https://openweathermap.org/city/${weatherData.id}`; let displayCityName = usingFallbackCoords ? 'Porto Alegre' : weatherData.name; let displayCountry = usingFallbackCoords ? 'BR' : weatherData.sys.country; const windSpeedKmH = (weatherData.wind.speed * 3.6).toFixed(1); const { description: windFeedbackText, className: windFeedbackClass } = getWindFeedback(parseFloat(windSpeedKmH)); const { description: pressureFeedbackText, className: pressureFeedbackClass } = getPressureFeedback(weatherData.main.pressure, weatherData.main.humidity, weatherData.clouds.all, hasRainToday); const { description: humidityFeedbackText, className: humidityFeedbackClass } = getHumidityFeedback(weatherData.main.humidity); weatherDetailsContainer.innerHTML = ` <strong>🌍 ${displayCityName}, ${displayCountry}</strong><br> 🌡️ ${convertTemp(weatherData.main.temp).toFixed(1)}${getUnitSymbol()} (mín: ${convertTemp(weatherData.main.temp_min).toFixed(1)}${getUnitSymbol()}, máx: ${convertTemp(weatherData.main.temp_max).toFixed(1)}${getUnitSymbol()})<br> 🤗 Sensação: ${convertTemp(weatherData.main.feels_like).toFixed(1)}${getUnitSymbol()}<br> 💧 Umidade: ${weatherData.main.humidity}%<br> <div class="humidity-info ${humidityFeedbackClass}">💧 ${humidityFeedbackText}</div> 🌬️ Vento: ${windSpeedKmH} km/h (${weatherData.wind.deg}º ${getWindDirectionCardinal(weatherData.wind.deg)})<br> <div class="wind-info ${windFeedbackClass}">💨 ${windFeedbackText}</div> 🌥️ ${weatherData.weather[0].description}<br> <div class="current-day-rain-info">🌧️ ${currentDayRainMessage}</div> ☁️ Nuvens: ${weatherData.clouds.all}%<br> 📈 Pressão: ${weatherData.main.pressure} hPa<br> <div class="pressure-info ${pressureFeedbackClass}">${pressureFeedbackText}</div> 🌅 Nascer: ${formatTime(weatherData.sys.sunrise)} | 🌇 Pôr: ${formatTime(weatherData.sys.sunset)}<br> <div class="forecast">${forecastHtml}</div>`; refreshButton.disabled = false; unitToggleButton.disabled = false; panel.style.height = 'auto'; } catch (error) { console.error("Erro ao carregar os dados do clima:", error); weatherDetailsContainer.innerHTML = ` <p style="color: red; text-align: center; padding: 10px;"> Não foi possível carregar os dados do clima. <br>Verifique sua conexão ou tente novamente mais tarde. <br>Detalhes: ${error.message} </p>`; panel.style.height = 'auto'; refreshButton.disabled = false; unitToggleButton.disabled = false; if (initialLoad) updateButtonDisplay(0.0, '50d', 'Erro!'); panel.querySelector('.refresh-btn')?.addEventListener('click', handleRefreshOrUnitToggle); } } async function initializeWeather() { let useCached = false; if (typeof GM_setValue !== 'undefined') { const cachedWeatherData = GM_getValue(CACHE_KEY_WEATHER_DATA); const lastUpdate = GM_getValue(CACHE_KEY_LAST_UPDATE, 0); const cachedCoordsType = GM_getValue(CACHE_KEY_COORDS_TYPE, 'gps'); if (cachedWeatherData && (Date.now() - lastUpdate < CACHE_EXPIRATION_MS)) { try { const parsedData = JSON.parse(cachedWeatherData); coords = { latitude: parsedData.latitude, longitude: parsedData.longitude }; usingFallbackCoords = (cachedCoordsType === 'fallback'); updateButtonDisplay(parsedData.temp, parsedData.icon, parsedData.description); refreshButton.disabled = false; unitToggleButton.disabled = false; weatherDetailsContainer.innerHTML = `Carregando detalhes do painel...`; useCached = true; updateWeather(parsedData.latitude, parsedData.longitude).catch(err => console.error("Erro ao atualizar o clima em segundo plano do cache:", err)); } catch (e) { console.error("Erro ao parsear dados do cache:", e); weatherDetailsContainer.innerHTML = 'Carregando...'; } } } if (!useCached) { weatherDetailsContainer.innerHTML = 'Carregando...'; navigator.geolocation.getCurrentPosition( async (pos) => { coords = pos.coords; usingFallbackCoords = false; await updateWeather(coords.latitude, coords.longitude, true); }, async (error) => { console.warn("Erro de geolocalização, usando coordenadas de fallback:", error); coords = fallbackCoords; usingFallbackCoords = true; await updateWeather(coords.latitude, coords.longitude, true); } ); } } initializeWeather(); })();