Files
2026-05-25 09:45:08 +00:00

748 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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&current=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())