import sys import os import ctypes import math import random import json import datetime import traceback import requests from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtGui import QPixmap, QPainter, QColor, QIcon, QFont # Hide console window on Windows immediately if running as compiled executable def hide_console(): try: hwnd = ctypes.windll.kernel32.GetConsoleWindow() if hwnd: ctypes.windll.user32.ShowWindow(hwnd, 0) # SW_HIDE = 0 except Exception: pass if getattr(sys, 'frozen', False): hide_console() base_dir = os.path.dirname(sys.executable) else: base_dir = os.path.dirname(os.path.abspath(__file__)) # Redirect stdout and stderr to a log file log_path = os.path.join(base_dir, "widget_output.log") try: log_file = open(log_path, "a", encoding="utf-8") sys.stdout = log_file sys.stderr = log_file except Exception: class DummyWriter: def write(self, x): pass def flush(self): pass sys.stdout = DummyWriter() sys.stderr = DummyWriter() # Weather descriptions in Russian WEATHER_DESCRIPTIONS = { 0: "Ясно", 1: "Преимущественно ясно", 2: "Переменная облачность", 3: "Пасмурно", 45: "Туман", 48: "Осаждающийся туман", 51: "Слабая морось", 53: "Умеренная морось", 55: "Плотная морось", 56: "Слабая ледяная морось", 57: "Плотная ледяная морось", 61: "Слабый дождь", 63: "Умеренный дождь", 65: "Сильный дождь", 66: "Слабый ледяной дождь", 67: "Сильный ледяной дождь", 71: "Слабый снегопад", 73: "Умеренный снегопад", 75: "Сильный снегопад", 77: "Снежные зерна", 80: "Слабый ливневый дождь", 81: "Умеренный ливневый дождь", 82: "Сильный ливневый дождь", 85: "Слабый снежный ливень", 86: "Сильный снежный ливень", 95: "Гроза", 96: "Гроза со слабым градом", 99: "Гроза с сильным градом" } # Drawing helpers for weather elements def draw_cloud(painter, x, y, size, color, opacity=1.0): painter.save() painter.setPen(QtCore.Qt.PenStyle.NoPen) c = QColor(color) if opacity < 1.0: c.setAlpha(int(opacity * 255)) painter.setBrush(QtGui.QBrush(c)) r1 = size * 0.28 r2 = size * 0.38 r3 = size * 0.28 painter.drawEllipse(x + size*0.05, y + size*0.25, r1*2, r1*2) painter.drawEllipse(x + size*0.2, y + size*0.05, r2*2, r2*2) painter.drawEllipse(x + size*0.48, y + size*0.25, r3*2, r3*2) painter.drawRoundedRect(x + size*0.1, y + size*0.3, size*0.55, size*0.26, size*0.13, size*0.13) painter.restore() def draw_sun(painter, cx, cy, r, angle): painter.save() # Center circle grad = QtGui.QLinearGradient(cx - r, cy - r, cx + r, cy + r) grad.setColorAt(0, QColor("#ffe259")) grad.setColorAt(1, QColor("#ffa751")) painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(grad) painter.drawEllipse(cx - r, cy - r, r*2, r*2) # Rays painter.setPen(QtGui.QPen(QColor("#ffa751"), max(1.5, r * 0.18), QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap)) for i in range(8): deg = angle + i * 45 rad = math.radians(deg) x1 = cx + (r * 1.28) * math.cos(rad) y1 = cy + (r * 1.28) * math.sin(rad) x2 = cx + (r * 1.58) * math.cos(rad) y2 = cy + (r * 1.58) * math.sin(rad) painter.drawLine(x1, y1, x2, y2) painter.restore() def draw_moon(painter, cx, cy, r, float_y): painter.save() path = QtGui.QPainterPath() path.addEllipse(cx - r, cy - r + float_y, r*2, r*2) cut_path = QtGui.QPainterPath() cut_path.addEllipse(cx - r * 0.3, cy - r + float_y, r*2, r*2) moon_path = path.subtracted(cut_path) painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(QColor("#e2e8f0")) painter.drawPath(moon_path) painter.restore() # Vector-animated custom weather icon widget class AnimatedWeatherIcon(QtWidgets.QWidget): def __init__(self, parent=None, size=48, auto_start=True): super().__init__(parent) self.setFixedSize(size, size) self.size_val = size self.weather_code = 3 self.is_day = True self.time_val = random.random() * 100.0 # offset start to desynchronize clouds self.sun_angle = random.randint(0, 360) # Slanted Rain drop offsets self.rain_offsets = [0.0, 5.0, 10.0] # Snow flake offsets self.snow_offsets = [0.0, 6.0, 3.0] self.lightning_timer = 0 self.lightning_visible = False self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.animate) if auto_start: self.timer.start(33) def set_weather(self, code, is_day): self.weather_code = code self.is_day = is_day self.update() def start_animation(self): if not self.timer.isActive(): self.timer.start(33) def stop_animation(self): if self.timer.isActive(): self.timer.stop() def animate(self): self.time_val += 0.06 self.sun_angle = (self.sun_angle + 0.8) % 360.0 # Rain animation for i in range(len(self.rain_offsets)): self.rain_offsets[i] += 0.5 if self.rain_offsets[i] > 12.0: self.rain_offsets[i] = 0.0 # Snow animation for i in range(len(self.snow_offsets)): self.snow_offsets[i] += 0.3 if self.snow_offsets[i] > 12.0: self.snow_offsets[i] = 0.0 # Lightning flash animation self.lightning_timer += 1 if self.lightning_visible: if self.lightning_timer > 3: self.lightning_visible = False self.lightning_timer = 0 else: if self.lightning_timer > 80 and random.random() < 0.04: self.lightning_visible = True self.lightning_timer = 0 self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) w = self.width() h = self.height() cx = w / 2 cy = h / 2 code = self.weather_code if code == 0: weather_type = "clear" elif code in (1, 2): weather_type = "partly_cloudy" elif code == 3: weather_type = "cloudy" elif code in (45, 48): weather_type = "fog" elif code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82): weather_type = "rain" elif code in (71, 73, 75, 77, 85, 86): weather_type = "snow" elif code in (95, 96, 99): weather_type = "storm" else: weather_type = "cloudy" float_y = math.sin(self.time_val) * (w * 0.05) if weather_type == "clear": if self.is_day: draw_sun(painter, cx, cy, w * 0.28, self.sun_angle) else: draw_moon(painter, cx, cy, w * 0.28, float_y) elif weather_type == "partly_cloudy": if self.is_day: draw_sun(painter, cx + w*0.14, cy - h*0.12, w * 0.22, self.sun_angle) else: draw_moon(painter, cx + w*0.14, cy - h*0.12, w * 0.2, float_y) draw_cloud(painter, cx - w*0.3, cy - h*0.2 + float_y, w * 0.64, "#e0f2fe") elif weather_type == "cloudy": draw_cloud(painter, cx - w*0.14, cy - h*0.28 + float_y*0.5, w * 0.58, "#bae6fd", opacity=0.8) draw_cloud(painter, cx - w*0.3, cy - h*0.18 + float_y, w * 0.66, "#ffffff") elif weather_type == "fog": draw_cloud(painter, cx - w*0.24, cy - h*0.26 + float_y, w * 0.6, "#e0f2fe", opacity=0.8) painter.save() painter.setPen(QtGui.QPen(QColor("#bae6fd"), max(1.5, w * 0.06), QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap)) drift = math.sin(self.time_val * 0.6) * (w * 0.06) painter.drawLine(cx - w*0.3 + drift, cy + h*0.18, cx + w*0.3 + drift, cy + h*0.18) painter.drawLine(cx - w*0.2 - drift, cy + h*0.28, cx + w*0.2 - drift, cy + h*0.28) painter.restore() elif weather_type == "rain": draw_cloud(painter, cx - w*0.3, cy - h*0.25 + float_y, w * 0.64, "#bae6fd") painter.save() painter.setPen(QtGui.QPen(QColor("#38bdf8"), max(1.5, w * 0.05), QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap)) for i, offset in enumerate(self.rain_offsets): rx = cx - w*0.15 + i * (w*0.15) ry = cy + h*0.14 + offset if ry < cy + h*0.42: painter.drawLine(rx, ry, rx - w*0.03, ry + h*0.12) painter.restore() elif weather_type == "snow": draw_cloud(painter, cx - w*0.3, cy - h*0.25 + float_y, w * 0.64, "#e0f2fe") painter.save() painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(QColor("#38bdf8")) for i, offset in enumerate(self.snow_offsets): sx = cx - w*0.15 + i * (w*0.15) + math.sin(self.time_val + i) * (w * 0.03) sy = cy + h*0.14 + offset if sy < cy + h*0.42: painter.drawEllipse(sx - w*0.04, sy - w*0.04, w*0.08, w*0.08) painter.restore() elif weather_type == "storm": draw_cloud(painter, cx - w*0.3, cy - h*0.25 + float_y, w * 0.64, "#94a3b8") if self.lightning_visible: painter.save() painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(QColor("#fbbf24")) scale = w / 48.0 points = [ QtCore.QPoint(cx - 2*scale, cy + 6*scale), QtCore.QPoint(cx - 10*scale, cy + 18*scale), QtCore.QPoint(cx - 3*scale, cy + 18*scale), QtCore.QPoint(cx - 7*scale, cy + 30*scale), QtCore.QPoint(cx + 5*scale, cy + 14*scale), QtCore.QPoint(cx - 1*scale, cy + 14*scale), ] painter.drawPolygon(points) painter.restore() # Color themes list: Background gradients + Borders THEMES = [ # Theme 0: Deep Violet/Indigo (Default) "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #121026, stop:1 #1c183a); border: 1px solid rgba(255, 255, 255, 0.08);", # Theme 1: Deep Navy Blue "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #09111e, stop:1 #14244a); border: 1px solid rgba(255, 255, 255, 0.08);", # Theme 2: Forest Emerald Green "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #04160f, stop:1 #0c2d1e); border: 1px solid rgba(255, 255, 255, 0.08);", # Theme 3: Dark Ruby Red "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1b0707, stop:1 #351212); border: 1px solid rgba(255, 255, 255, 0.08);", # Theme 4: Sleek Charcoal Black "background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #121212, stop:1 #242424); border: 1px solid rgba(255, 255, 255, 0.08);" ] class WeatherWidget(QtWidgets.QWidget): def __init__(self): super().__init__() self.drag_position = QtCore.QPoint() self.press_pos = QtCore.QPoint() self.press_time = datetime.datetime.now() self.expanded = False self.theme_index = 0 self.init_ui() self.load_config() # Setup tray icon self.tray_icon = QtWidgets.QSystemTrayIcon(self) self.tray_icon.setIcon(self.create_tray_icon()) tray_menu = QtWidgets.QMenu() refresh_action = tray_menu.addAction("Обновить") refresh_action.triggered.connect(self.update_weather) exit_action = tray_menu.addAction("Выход") exit_action.triggered.connect(self.close_app) self.tray_icon.setContextMenu(tray_menu) self.tray_icon.show() # Setup update timer (10 minutes) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.update_weather) self.timer.start(10 * 60 * 1000) # Setup startup fade-in animation self.setWindowOpacity(0.0) self.fade_in_animation = QtCore.QPropertyAnimation(self, b"windowOpacity") self.fade_in_animation.setDuration(400) self.fade_in_animation.setStartValue(0.0) self.fade_in_animation.setEndValue(1.0) self.fade_in_animation.start() # Initial weather load self.update_weather() def get_config_path(self): if getattr(sys, 'frozen', False): base_dir = os.path.dirname(sys.executable) else: base_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_dir, "widget_config.json") def save_config(self): try: config = { "x": self.x(), "y": self.y(), "theme_index": self.theme_index } with open(self.get_config_path(), "w") as f: json.dump(config, f) except Exception as e: print(f"Error saving config: {e}") def load_config(self): # Balanced proportions: width 280 x height 102 widget_width = 280 widget_height = 102 screen = QtWidgets.QApplication.primaryScreen() screen_geom = screen.availableGeometry() # Default position: bottom-right default_x = screen_geom.right() - widget_width - 20 default_y = screen_geom.bottom() - widget_height - 20 config_path = self.get_config_path() if os.path.exists(config_path): try: with open(config_path, "r") as f: config = json.load(f) x = config.get("x", default_x) y = config.get("y", default_y) self.theme_index = config.get("theme_index", 0) # Check if position is within current screen boundaries if 0 <= x < screen_geom.right() and 0 <= y < screen_geom.bottom(): self.setGeometry(x, y, widget_width, widget_height) self.apply_theme() return except Exception as e: print(f"Error loading config: {e}") self.setGeometry(default_x, default_y, widget_width, widget_height) self.apply_theme() def apply_theme(self): theme_style = THEMES[self.theme_index] self.container.setStyleSheet(f""" #container {{ {theme_style} border-radius: 22px; }} #container:hover {{ border-width: 1px; border-style: solid; border-color: rgba(255, 255, 255, 0.18); }} QLabel {{ color: #ffffff; }} """) def cycle_theme(self): self.theme_index = (self.theme_index + 1) % len(THEMES) self.apply_theme() self.save_config() def create_tray_icon(self): # Dynamically draw a tray icon (stylized clouds/sun) pixmap = QPixmap(16, 16) pixmap.fill(QtCore.Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setPen(QtCore.Qt.PenStyle.NoPen) # Background cloud circles painter.setBrush(QColor("#38bdf8")) painter.drawEllipse(1, 6, 8, 8) painter.drawEllipse(6, 4, 9, 9) painter.drawEllipse(4, 2, 8, 8) painter.end() return QIcon(pixmap) def init_ui(self): # Frameless, non-always-on-top, tool window (no taskbar button) self.setWindowFlags( QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Tool | QtCore.Qt.WindowType.Window ) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) # Outer layout to provide space for the drop shadow outer_layout = QtWidgets.QVBoxLayout(self) outer_layout.setContentsMargins(12, 12, 12, 12) # Container frame self.container = QtWidgets.QFrame() self.container.setObjectName("container") self.apply_theme() # Soft Drop Shadow shadow = QtWidgets.QGraphicsDropShadowEffect(self) shadow.setBlurRadius(15) shadow.setXOffset(0) shadow.setYOffset(4) shadow.setColor(QColor(0, 0, 0, 120)) self.container.setGraphicsEffect(shadow) # Layout inside container (QVBoxLayout to hold current weather + divider + forecast) container_layout = QtWidgets.QVBoxLayout(self.container) container_layout.setContentsMargins(16, 8, 16, 8) container_layout.setSpacing(6) # Top Row: Current weather layout (QHBoxLayout) top_layout = QtWidgets.QHBoxLayout() top_layout.setSpacing(12) # Left Side: Weather Icon (beautiful vector-animated) self.icon_label = AnimatedWeatherIcon(self.container, size=48, auto_start=True) top_layout.addWidget(self.icon_label, alignment=QtCore.Qt.AlignmentFlag.AlignCenter) # Right Side: Content right_layout = QtWidgets.QVBoxLayout() right_layout.setSpacing(0) # Row 1: Updated Time (aligned to top-right) time_layout = QtWidgets.QHBoxLayout() time_layout.addStretch() self.update_time_label = QtWidgets.QLabel("обновление...") self.update_time_label.setFont(QFont("Segoe UI", 7)) self.update_time_label.setStyleSheet("color: rgba(255, 255, 255, 0.45);") time_layout.addWidget(self.update_time_label) right_layout.addLayout(time_layout) # Row 2: Temperature self.temp_label = QtWidgets.QLabel("--°") self.temp_label.setFont(QFont("Segoe UI", 24, QFont.Weight.Bold)) self.temp_label.setStyleSheet("line-height: 1;") right_layout.addWidget(self.temp_label) # Row 3: Condition text self.condition_label = QtWidgets.QLabel("Загрузка...") self.condition_label.setFont(QFont("Segoe UI", 9.5, QFont.Weight.DemiBold)) self.condition_label.setStyleSheet("color: rgba(255, 255, 255, 0.9);") right_layout.addWidget(self.condition_label) # Row 4: Detailed info (apparent, wind, humidity) self.details_label = QtWidgets.QLabel("ощущ. --° | -- м/с | --%") self.details_label.setFont(QFont("Segoe UI", 8)) self.details_label.setStyleSheet("color: rgba(255, 255, 255, 0.55);") right_layout.addWidget(self.details_label) top_layout.addLayout(right_layout) container_layout.addLayout(top_layout) # Divider Line self.divider = QtWidgets.QFrame() self.divider.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.divider.setStyleSheet("background-color: rgba(255, 255, 255, 0.1); max-height: 1px; border: none; margin-top: 2px; margin-bottom: 2px;") container_layout.addWidget(self.divider) # Forecast Panel self.forecast_panel = QtWidgets.QWidget() self.forecast_panel.setObjectName("forecast_panel") forecast_layout = QtWidgets.QHBoxLayout(self.forecast_panel) forecast_layout.setContentsMargins(0, 4, 0, 4) forecast_layout.setSpacing(8) self.forecast_days = [] for i in range(3): # Premium Glassmorphic cards for forecast ("нано-банану") day_widget = QtWidgets.QFrame() day_widget.setObjectName("forecast_card") day_widget.setStyleSheet(""" #forecast_card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 10px; } #forecast_card:hover { background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); } """) day_widget_layout = QtWidgets.QVBoxLayout(day_widget) day_widget_layout.setContentsMargins(4, 6, 4, 6) day_widget_layout.setSpacing(2) day_widget_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) day_label = QtWidgets.QLabel("---") day_label.setFont(QFont("Segoe UI", 8, QFont.Weight.Bold)) day_label.setStyleSheet("color: rgba(255, 255, 255, 0.6);") day_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) # Forecast icon (stops drawing CPU when collapsed) icon_widget = AnimatedWeatherIcon(day_widget, size=32, auto_start=False) temp_label = QtWidgets.QLabel("--° / --°") temp_label.setFont(QFont("Segoe UI", 7.5)) temp_label.setStyleSheet("color: rgba(255, 255, 255, 0.95);") temp_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) day_widget_layout.addWidget(day_label) day_widget_layout.addWidget(icon_widget, alignment=QtCore.Qt.AlignmentFlag.AlignCenter) day_widget_layout.addWidget(temp_label) forecast_layout.addWidget(day_widget) self.forecast_days.append({ "day_label": day_label, "icon_label": icon_widget, "temp_label": temp_label }) container_layout.addWidget(self.forecast_panel) # Set forecast initial state self.forecast_panel.setVisible(False) self.divider.setVisible(False) outer_layout.addWidget(self.container) # Mouse dragging and closing implementation def mousePressEvent(self, event): if event.button() == QtCore.Qt.MouseButton.RightButton: self.close_app() event.accept() elif event.button() == QtCore.Qt.MouseButton.LeftButton: self.press_pos = event.globalPosition().toPoint() self.press_time = datetime.datetime.now() self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): if event.buttons() == QtCore.Qt.MouseButton.LeftButton: self.move(event.globalPosition().toPoint() - self.drag_position) event.accept() def mouseReleaseEvent(self, event): if event.button() == QtCore.Qt.MouseButton.LeftButton: self.save_config() # Detect left click for expand/collapse release_pos = event.globalPosition().toPoint() distance = (release_pos - self.press_pos).manhattanLength() duration = (datetime.datetime.now() - self.press_time).total_seconds() if distance < 5 and duration < 0.25: self.toggle_expand() event.accept() def mouseDoubleClickEvent(self, event): if event.button() == QtCore.Qt.MouseButton.LeftButton: self.cycle_theme() event.accept() def toggle_expand(self): self.expanded = not self.expanded # Animate geometry height change (collapsed 102 -> expanded 192) start_geom = self.geometry() target_height = 192 if self.expanded else 102 end_geom = QtCore.QRect(start_geom.x(), start_geom.y(), start_geom.width(), target_height) self.resize_anim = QtCore.QPropertyAnimation(self, b"geometry") self.resize_anim.setDuration(220) self.resize_anim.setEasingCurve(QtCore.QEasingCurve.Type.InOutQuad) self.resize_anim.setStartValue(start_geom) self.resize_anim.setEndValue(end_geom) if self.expanded: # Show widgets and start forecast animations self.divider.setVisible(True) self.forecast_panel.setVisible(True) for item in self.forecast_days: item["icon_label"].start_animation() self.resize_anim.start() else: # Hide forecast elements FIRST to release size constraints, allowing successful window shrink self.divider.setVisible(False) self.forecast_panel.setVisible(False) for item in self.forecast_days: item["icon_label"].stop_animation() self.resize_anim.start() # Update weather fetch (with fade transition) def update_weather(self): self.fade_anim = QtCore.QPropertyAnimation(self, b"windowOpacity") self.fade_anim.setDuration(250) self.fade_anim.setStartValue(self.windowOpacity()) self.fade_anim.setEndValue(0.3) def on_fade_out(): self.perform_weather_fetch() self.fade_in_anim = QtCore.QPropertyAnimation(self, b"windowOpacity") self.fade_in_anim.setDuration(250) self.fade_in_anim.setStartValue(0.3) self.fade_in_anim.setEndValue(1.0) self.fade_in_anim.start() self.fade_anim.finished.connect(on_fade_out) self.fade_anim.start() def perform_weather_fetch(self): # API URL for Krasnoyarsk coordinates with daily variables included url = "https://api.open-meteo.com/v1/forecast?latitude=56.0184&longitude=92.8672¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&wind_speed_unit=ms&timezone=Asia%2FKrasnoyarsk" try: response = requests.get(url, timeout=10) if response.status_code == 200: data = response.json() current = data.get("current", {}) # Current Temperature temp = round(current.get("temperature_2m", 0)) temp_str = f"+{temp}°" if temp > 0 else f"{temp}°" self.temp_label.setText(temp_str) # Current Weather code & description code = current.get("weather_code", 3) condition = WEATHER_DESCRIPTIONS.get(code, "Пасмурно") self.condition_label.setText(condition) # Current Apparent temp, wind, humidity app_temp = round(current.get("apparent_temperature", 0)) app_temp_str = f"+{app_temp}°" if app_temp > 0 else f"{app_temp}°" wind = round(current.get("wind_speed_10m", 0)) humidity = current.get("relative_humidity_2m", 0) self.details_label.setText(f"ощущ. {app_temp_str} | {wind} м/с | {humidity}%") # Current Animated Weather Icon is_day = current.get("is_day", 1) == 1 self.icon_label.set_weather(code, is_day) # Last updated timestamp now = datetime.datetime.now() self.update_time_label.setText(f"обновлено {now.strftime('%H:%M')}") # Parse 3-day forecast (indices 1, 2, 3 in daily data) daily = data.get("daily", {}) daily_times = daily.get("time", []) daily_codes = daily.get("weather_code", []) daily_maxs = daily.get("temperature_2m_max", []) daily_mins = daily.get("temperature_2m_min", []) for i in range(1, 4): if i < len(daily_times): date_str = daily_times[i] f_code = daily_codes[i] max_temp = round(daily_maxs[i]) min_temp = round(daily_mins[i]) # Get weekday name in Russian dt = datetime.datetime.strptime(date_str, "%Y-%m-%d") weekdays_ru = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] day_name = weekdays_ru[dt.weekday()] # Format forecast temps max_str = f"+{max_temp}°" if max_temp > 0 else f"{max_temp}°" min_str = f"+{min_temp}°" if min_temp > 0 else f"{min_temp}°" # Apply to forecast labels self.forecast_days[i-1]["day_label"].setText(day_name) self.forecast_days[i-1]["temp_label"].setText(f"{max_str} / {min_str}") self.forecast_days[i-1]["icon_label"].set_weather(f_code, True) else: self.set_offline_status("Ошибка сервера") except Exception as e: print(f"Fetch error: {e}") self.set_offline_status("Нет сети") def set_offline_status(self, message): self.condition_label.setText(message) self.temp_label.setText("--°") self.details_label.setText("ощущ. --° | -- м/с | --%") self.icon_label.set_weather(3, True) # Show cloud for item in self.forecast_days: item["day_label"].setText("---") item["temp_label"].setText("--° / --°") item["icon_label"].set_weather(3, True) def close_app(self): self.tray_icon.hide() QtWidgets.QApplication.quit() if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) # Ensure app doesn't close when main window is hidden/minimized app.setQuitOnLastWindowClosed(False) widget = WeatherWidget() widget.show() sys.exit(app.exec())