From bcbd5843664f72d019889ce5d7c5ad978e94ef2b Mon Sep 17 00:00:00 2001 From: queenkjuul Date: Sun, 10 Nov 2024 08:17:34 -0600 Subject: [PATCH 1/4] Add basic weather information to screensaver Get basic temperature, conditions, location working with scaled icon Code cleanup Add IP Geolocation to weather component Cleanup, settings integrations, bugfixes, translation Fix Localization cleanup --- cinnamon-screensaver.pot | 54 ++++---- src/meson.build | 1 + src/stage.py | 32 +++++ src/util/geojs.py | 24 ++++ src/util/location.py | 19 +++ src/util/meson.build | 7 +- src/util/openweathermap.py | 271 +++++++++++++++++++++++++++++++++++++ src/util/settings.py | 22 +++ src/util/weather.py | 6 + src/util/weather_types.py | 194 ++++++++++++++++++++++++++ src/weather.py | 133 ++++++++++++++++++ 11 files changed, 737 insertions(+), 26 deletions(-) create mode 100644 src/util/geojs.py create mode 100644 src/util/location.py create mode 100644 src/util/openweathermap.py create mode 100644 src/util/weather.py create mode 100644 src/util/weather_types.py create mode 100644 src/weather.py diff --git a/cinnamon-screensaver.pot b/cinnamon-screensaver.pot index 3fef178b..2f41a6b2 100644 --- a/cinnamon-screensaver.pot +++ b/cinnamon-screensaver.pot @@ -8,61 +8,61 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-02 21:29+0000\n" +"POT-Creation-Date: 2024-11-22 04:11-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: src/cinnamon-screensaver-command.py:41 +#: src/cinnamon-screensaver-command.py:43 msgid "Causes the screensaver to exit gracefully" msgstr "" -#: src/cinnamon-screensaver-command.py:43 +#: src/cinnamon-screensaver-command.py:45 msgid "Query the state of the screensaver" msgstr "" -#: src/cinnamon-screensaver-command.py:45 +#: src/cinnamon-screensaver-command.py:47 msgid "Query the length of time the screensaver has been active" msgstr "" -#: src/cinnamon-screensaver-command.py:47 +#: src/cinnamon-screensaver-command.py:49 msgid "Tells the running screensaver process to lock the screen immediately" msgstr "" -#: src/cinnamon-screensaver-command.py:49 +#: src/cinnamon-screensaver-command.py:51 msgid "Turn the screensaver on (blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:51 +#: src/cinnamon-screensaver-command.py:53 msgid "If the screensaver is active then deactivate it (un-blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:53 +#: src/cinnamon-screensaver-command.py:55 msgid "Version of this application" msgstr "" -#: src/cinnamon-screensaver-command.py:55 +#: src/cinnamon-screensaver-command.py:57 msgid "Message to be displayed in lock screen" msgstr "" -#: src/cinnamon-screensaver-command.py:105 +#: src/cinnamon-screensaver-command.py:106 msgid "The screensaver is active\n" msgstr "" -#: src/cinnamon-screensaver-command.py:107 +#: src/cinnamon-screensaver-command.py:108 msgid "The screensaver is inactive\n" msgstr "" -#: src/cinnamon-screensaver-command.py:111 +#: src/cinnamon-screensaver-command.py:112 msgid "The screensaver is not currently active.\n" msgstr "" -#: src/cinnamon-screensaver-command.py:113 +#: src/cinnamon-screensaver-command.py:114 #, python-format msgid "The screensaver has been active for %d second.\n" msgid_plural "The screensaver has been active for %d seconds.\n" @@ -80,7 +80,7 @@ msgid "" "prior to this occurring." msgstr "" -#: src/passwordEntry.py:23 src/unlock.py:216 +#: src/passwordEntry.py:23 src/unlock.py:215 msgid "Please enter your password..." msgstr "" @@ -92,52 +92,56 @@ msgstr "" msgid "Switch User" msgstr "" -#: src/unlock.py:189 +#: src/unlock.py:188 msgid "Incorrect password" msgstr "" -#: src/unlock.py:206 +#: src/unlock.py:205 msgid "Checking..." msgstr "" -#: src/unlock.py:250 +#: src/unlock.py:249 msgid "You have the Caps Lock key on." msgstr "" +#: src/weather.py:102 +msgid "in" +msgstr "" + #. This is the first line of text for the backup-locker, explaining how to switch to tty #. and run 'cinnamon-unlock-desktop' command. This appears if the screensaver crashes. -#: backup-locker/cs-backup-locker.c:255 +#: backup-locker/cs-backup-locker.c:306 msgid "Something went wrong with the screensaver." msgstr "" #. (continued) This is a subtitle -#: backup-locker/cs-backup-locker.c:265 +#: backup-locker/cs-backup-locker.c:316 msgid "We'll help you get your desktop back" msgstr "" #. (new section) Bulleted list of steps to take to unlock the desktop; -#: backup-locker/cs-backup-locker.c:276 +#: backup-locker/cs-backup-locker.c:327 #, c-format msgid "Switch to a console using ." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:278 +#: backup-locker/cs-backup-locker.c:329 msgid "Log in by typing your user name followed by your password." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:280 +#: backup-locker/cs-backup-locker.c:331 msgid "At the prompt, type 'cinnamon-unlock-desktop' and press Enter." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:282 +#: backup-locker/cs-backup-locker.c:333 #, c-format msgid "Switch back to your unlocked desktop using ." msgstr "" #. (end section) Final words after the list of steps -#: backup-locker/cs-backup-locker.c:287 +#: backup-locker/cs-backup-locker.c:338 msgid "If you can reproduce this behavior, please file a report here:" msgstr "" diff --git a/src/meson.build b/src/meson.build index dd86840b..bb2d1881 100644 --- a/src/meson.build +++ b/src/meson.build @@ -32,6 +32,7 @@ app_py = [ 'status.py', 'unlock.py', 'volumeControl.py', + 'weather.py' ] app_css = [ diff --git a/src/stage.py b/src/stage.py index c73440c9..437d50f3 100644 --- a/src/stage.py +++ b/src/stage.py @@ -20,6 +20,7 @@ from util import utils, trackers, settings from util.eventHandler import EventHandler from util.utils import DEBUG +from weather import WeatherWidget class Stage(Gtk.Window): """ @@ -69,6 +70,7 @@ def __init__(self, manager, away_message): self.overlay = None self.clock_widget = None self.albumart_widget = None + self.weather_widget = None self.unlock_dialog = None self.audio_panel = None self.info_panel = None @@ -291,6 +293,11 @@ def setup_delayed_components(self, data=None): except Exception as e: print("Problem setting up albumart widget: %s" % str(e)) self.albumart_widget = None + try: + self.setup_weather() + except Exception as e: + print("Problem setting up weather widget: %s" % str(e)) + self.weather_widget = None try: self.setup_status_bars() except Exception as e: @@ -324,6 +331,13 @@ def destroy_children(self): except Exception as e: print(e) + try: + if self.weather_widget is not None: + self.weather_widget.stop_positioning() + self.weather_widget.destroy() + except Exception as e: + print(e) + try: if self.info_panel is not None: self.info_panel.destroy() @@ -345,6 +359,7 @@ def destroy_children(self): self.unlock_dialog = None self.clock_widget = None self.albumart_widget = None + self.weather_widget = None self.info_panel = None self.audio_panel = None self.osk = None @@ -504,6 +519,23 @@ def setup_albumart(self): if settings.get_show_albumart(): self.albumart_widget.start_positioning() + def setup_weather(self): + """ + Construct the Weather widget and add it to the overlay, but only actually + show it if we're a) Not running a plug-in, and b) The user wants it via + preferences. + + Initially invisible, regardless - its visibility is controlled via its + own positioning timer. + """ + self.weather_widget = WeatherWidget(None, status.screen.get_mouse_monitor()) + self.add_child_widget(self.weather_widget) + + self.floaters.append(self.weather_widget) + + if settings.get_show_albumart(): + self.weather_widget.start_positioning() + def setup_osk(self): self.osk = OnScreenKeyboard() diff --git a/src/util/geojs.py b/src/util/geojs.py new file mode 100644 index 00000000..e5458916 --- /dev/null +++ b/src/util/geojs.py @@ -0,0 +1,24 @@ +import json +from types import SimpleNamespace + +import requests + +from util.location import LocationProvider, LocationData + +URL = "https://get.geojs.io/v1/ip/geo.json" + +class GeoJSLocationProvider(LocationProvider): + """ + LocationProvider implementation for geojs.io + """ + + def __init__(self): + pass + + @staticmethod + def GetLocation() -> LocationData: + response = requests.get(URL) + + data = json.loads(response.text, object_hook=lambda d: SimpleNamespace(**d)) + + return LocationData(float(data.latitude), float(data.longitude), data.city, data.country, data.timezone, data.city) \ No newline at end of file diff --git a/src/util/location.py b/src/util/location.py new file mode 100644 index 00000000..b5204b9b --- /dev/null +++ b/src/util/location.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class LocationData: + lat: float + lon: float + city: Optional[str] = None + country: Optional[str] = None + timeZone: Optional[str] = None + entryText: Optional[str] = None + + +class LocationProvider(ABC): + @abstractmethod + def GetLocation(self) -> LocationData: + pass diff --git a/src/util/meson.build b/src/util/meson.build index 393cacab..d2cdeb41 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -3,10 +3,15 @@ app_py = [ 'eventHandler.py', 'fader.py', 'focusNavigator.py', + 'geojs.py', 'keybindings.py', + 'location.py', + 'openweathermap.py', 'settings.py', 'trackers.py', - 'utils.py' + 'utils.py', + 'weather.py', + 'weather_types.py' ] install_data(app_py, install_dir: join_paths(pkgdatadir, 'util')) diff --git a/src/util/openweathermap.py b/src/util/openweathermap.py new file mode 100644 index 00000000..243c1bc9 --- /dev/null +++ b/src/util/openweathermap.py @@ -0,0 +1,271 @@ +import json +from types import SimpleNamespace + +import requests +from gi.repository import Pango + +from util.weather_types import ( + APIUniqueField, + BuiltinIcons, + Condition, + CustomIcons, + Location, + LocationData, + WeatherData, + WeatherProvider, + Wind, +) + +OWM_URL = "https://api.openweathermap.org/data/2.5/weather" +# this is the OpenWeatherMap API key used by linux-mint/cinnamon-spices-applets/weather@mockturtl +# presumably belongs to the org? +OWM_API_KEY = "1c73f8259a86c6fd43c7163b543c8640" +OWM_SUPPORTED_LANGS = [ + "af", + "al", + "ar", + "az", + "bg", + "ca", + "cz", + "da", + "de", + "el", + "en", + "eu", + "fa", + "fi", + "fr", + "gl", + "he", + "hi", + "hr", + "hu", + "id", + "it", + "ja", + "kr", + "la", + "lt", + "mk", + "no", + "nl", + "pl", + "pt", + "pt_br", + "ro", + "ru", + "se", + "sk", + "sl", + "sp", + "es", + "sr", + "th", + "tr", + "ua", + "uk", + "vi", + "zh_cn", + "zh_tw", + "zu", +] + + +class OWMWeatherProvider(WeatherProvider): + """ + WeatherProvider implementation for OpenWeatherMap.org + """ + + def __init__(self): + self.needsApiKey = False + self.prettyName = _("OpenWeatherMap") + self.name = "OpenWeatherMap_Open" + self.maxForecastSupport = 7 + self.maxHourlyForecastSupport = 0 + self.website = "https://openweathermap.org/" + self.remainingCalls = None + self.supportHourlyPrecipChance = False + self.supportHourlyPrecipVolume = False + + def GetWeather(self, loc: LocationData): + lang = self.locale_to_owm_lang(Pango.language_get_default().to_string()) + pref = list( + map( + lambda p: self.locale_to_owm_lang(p.to_string()), + Pango.language_get_preferred(), + ) + ) + if lang not in OWM_SUPPORTED_LANGS: + for locale in pref: + if self.locale_to_owm_lang(locale) in OWM_SUPPORTED_LANGS: + lang = self.locale_to_owm_lang(locale) + break + # if we still have not found a supported language... + if lang not in OWM_SUPPORTED_LANGS: + lang = "en" + + response = requests.get( + OWM_URL, + { + "lat": loc.lat, + "lon": loc.lon, + "units": "standard", + "appid": OWM_API_KEY, + "lang": lang, + }, + ) + + # actual object structure: https://github.com/linuxmint/cinnamon-spices-applets/weather@mockturtl/src/3_8/providers/openweathermap/payload/weather.ts + data = json.loads(response.text, object_hook=lambda d: SimpleNamespace(**d)) + return self.owm_data_to_weather_data(data) + + @staticmethod + def locale_to_owm_lang(locale_string): + if locale_string is None: + return "en" + + # Dialect? support by OWM + if ( + locale_string == "zh-cn" + or locale_string == "zh-cn" + or locale_string == "pt-br" + ): + return locale_string + + lang = locale_string.split("-")[0] + # OWM uses different language code for Swedish, Czech, Korean, Latvian, Norwegian + if lang == "sv": + return "se" + elif lang == "cs": + return "cz" + elif lang == "ko": + return "kr" + elif lang == "lv": + return "la" + elif lang == "nn" or lang == "nb": + return "no" + return lang + + def owm_data_to_weather_data(self, owm_data) -> WeatherData: + """ + Returns as much of a complete WeatherData object as we can + """ + return WeatherData( + **dict( + date=owm_data.dt, + sunrise=owm_data.sys.sunrise, + sunset=owm_data.sys.sunset, + coord=owm_data.coord, + location=Location( + **dict( + city=owm_data.name, + country=owm_data.sys.country, + url="https://openweathermap.org/city/%s" % owm_data.id, + ) + ), + condition=Condition( + **dict( + main=owm_data.weather[0].main, + description=owm_data.weather[0].description, + icons=self.owm_icon_to_builtin_icons(owm_data.weather[0].icon), + customIcon=self.owm_icon_to_custom_icon( + owm_data.weather[0].icon + ), + ) + ), + wind=Wind(**dict(speed=owm_data.wind.speed, degree=owm_data.wind.deg)), + temperature=owm_data.main.temp, + pressure=owm_data.main.pressure, + humidity=owm_data.main.humidity, + dewPoint=None, + extra_field=APIUniqueField( + **dict( + type="temperature", + name=_("Feels Like"), + value=owm_data.main.feels_like, + ) + ), + ) + ) + + @staticmethod + def owm_icon_to_builtin_icons(icon) -> list[BuiltinIcons]: + # https://openweathermap.org/weather-conditions + # fallback icons are: weather-clear-night + # weather-clear weather-few-clouds-night weather-few-clouds + # weather-fog weather-overcast weather-severe-alert weather-showers + # weather-showers-scattered weather-snow weather-storm + match icon: + case "10d": + # rain day */ + return [ + "weather-rain", + "weather-showers-scattered", + "weather-freezing-rain", + ] + case "10n": + # rain night */ + return [ + "weather-rain", + "weather-showers-scattered", + "weather-freezing-rain", + ] + case "09n": + # showers night*/ + return ["weather-showers"] + case "09d": + # showers day */ + return ["weather-showers"] + case "13d": + # snow day*/ + return ["weather-snow"] + case "13n": + # snow night */ + return ["weather-snow"] + case "50d": + # mist day */ + return ["weather-fog"] + case "50n": + # mist night */ + return ["weather-fog"] + case "04d": + # broken clouds day */ + return ["weather-overcast", "weather-clouds", "weather-few-clouds"] + case "04n": + # broken clouds night */ + return [ + "weather-overcast", + "weather-clouds-night", + "weather-few-clouds-night", + ] + case "03n": + # mostly cloudy (night) */ + return ["weather-clouds-night", "weather-few-clouds-night"] + case "03d": + # mostly cloudy (day) */ + return ["weather-clouds", "weather-few-clouds", "weather-overcast"] + case "02n": + # partly cloudy (night) */ + return ["weather-few-clouds-night"] + case "02d": + # partly cloudy (day) */ + return ["weather-few-clouds"] + case "01n": + # clear (night) */ + return ["weather-clear-night"] + case "01d": + # sunny */ + return ["weather-clear"] + case "11d": + # storm day */ + return ["weather-storm"] + case "11n": + # storm night */ + return ["weather-storm"] + case _: + return ["weather-severe-alert"] + + @staticmethod + def owm_icon_to_custom_icon(icon) -> CustomIcons: + return None # TODO diff --git a/src/util/settings.py b/src/util/settings.py index 58a7c4fe..fdc3354f 100644 --- a/src/util/settings.py +++ b/src/util/settings.py @@ -30,6 +30,9 @@ SHOW_CLOCK_KEY = "show-clock" SHOW_ALBUMART = "show-album-art" +SHOW_WEATHER = "show-weather" +WEATHER_LOCATION = "weather-location" +WEATHER_UNITS = "weather-units" ALLOW_SHORTCUTS = "allow-keyboard-shortcuts" ALLOW_MEDIA_CONTROL = "allow-media-control" SHOW_INFO_PANEL = "show-info-panel" @@ -56,6 +59,7 @@ # "settings.ss_settings.get_string(settings.DEFAULT_MESSAGE_KEY)" or keeping # instances of GioSettings wherever we need them. + def _check_string(string): if string and string != "": return string @@ -133,6 +137,24 @@ def get_show_clock(): def get_show_albumart(): return ss_settings.get_boolean(SHOW_ALBUMART) +def get_show_weather(): + return ss_settings.get_boolean(SHOW_WEATHER) + +def get_weather_location(): + location_string = ss_settings.get_string(WEATHER_LOCATION) # string LAT,LON + return _check_string(location_string) + +def get_weather_units(): + units = ["metric", "imperial"] + units_string = _check_string(ss_settings.get_string(WEATHER_UNITS)) + return units_string if units_string in units else "metric" + +def get_weather_font(): + # reusing the Clock widget Time font for now (it's big) + time_font = ss_settings.get_string(FONT_TIME_KEY) + + return _check_string(time_font) + def get_allow_shortcuts(): return ss_settings.get_boolean(ALLOW_SHORTCUTS) diff --git a/src/util/weather.py b/src/util/weather.py new file mode 100644 index 00000000..e7ffd4e7 --- /dev/null +++ b/src/util/weather.py @@ -0,0 +1,6 @@ +def k_to_c(k: float) -> float: + return round(k - 273.15, 1) + + +def k_to_f(k: float) -> int: + return round((9 / 5 * (k - 273.15) + 32)) diff --git a/src/util/weather_types.py b/src/util/weather_types.py new file mode 100644 index 00000000..b3e257be --- /dev/null +++ b/src/util/weather_types.py @@ -0,0 +1,194 @@ +# Mostly derived from/compatible with: +# https://github.com/linuxmint/cinnamon-spices-applets/weather@mockturtl/src/3_8/types.ts +# we don't use much of the data, but intending for easy porting of sources + customizations going forward +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Literal, Optional + +from util.location import LocationData +from util.weather import k_to_c, k_to_f + +type PrecipitationTypes = Literal[ + "rain", "snow", "none", "ice pellets", "freezing rain" +] +type BuiltinIcons = Literal[ + "weather-clear", + "weather-clear-night", + "weather-few-clouds", + "weather-few-clouds-night", + "weather-clouds", + "weather-many-clouds", + "weather-overcast", + "weather-showers-scattered", + "weather-showers-scattered-day", + "weather-showers-scattered-night", + "weather-showers-day", + "weather-showers-night", + "weather-showers", + "weather-rain", + "weather-freezing-rain", + "weather-snow", + "weather-snow-day", + "weather-snow-night", + "weather-snow-rain", + "weather-snow-scattered", + "weather-snow-scattered-day", + "weather-snow-scattered-night", + "weather-storm", + "weather-hail", + "weather-fog", + "weather-tornado", + "weather-windy", + "weather-breeze", + "weather-clouds-night", + "weather-severe-alert", +] +type CustomIcons = Literal[None] # TODO + + +@dataclass +class Coord: + lat: float + lon: float + + +@dataclass +class Location: + city: Optional[str] = None + country: Optional[str] = None + timeZone: Optional[str] = None + url: Optional[str] = None + tzOffset: Optional[float] = None + + +@dataclass +class StationInfo: + distanceFrom: float + name: Optional[str] = None + lat: Optional[float] = None + lon: Optional[float] = None + area: Optional[str] = None + + +@dataclass +class Wind: + # m/s + speed: float + # meteorological degrees + degree: float + + +@dataclass +class Condition: + main: str + description: str + icons: Optional[list[BuiltinIcons]] = None + customIcon: Optional[CustomIcons] = None # TODO + + +class ForecastData: + date: int + temp_min: float # kelvin + temp_max: float # kelvin + condition: Condition + + +@dataclass +class Precipitation: + type: PrecipitationTypes + # /** in mm */ + volume: Optional[float] = None + # /** % */ + chance: Optional[float] = None + + +@dataclass +class HourlyForecastData: + date: int + # /** Kelvin */ + temp: float + condition: Condition + precipitation: Optional[Precipitation] = None + + +type APIUniqueFieldTypes = Literal["temperature", "percent", "string"] + + +@dataclass +class APIUniqueField: + name: str + value: str | float + type: APIUniqueFieldTypes + + +@dataclass +class ImmediatePrecipitation: + start: int + end: int + + +type AlertSeverity = Literal["minor", "moderate", "severe", "extreme", "unknown"] + + +@dataclass +class AlertData: + sender_name: str + level: AlertSeverity + title: str + description: str + icon: Optional[BuiltinIcons | CustomIcons] = None + + +@dataclass +class WeatherData: + date: int + coord: Coord + location: Location + condition: Condition + wind: Wind + stationInfo: Optional[StationInfo] = None + # /** in UTC with tz info */ + sunrise: Optional[float] = None + # /** in UTC with tz info */ + sunset: Optional[float] = None + # /** In Kelvin */ + temperature: Optional[float] = None + # /** In hPa */ + pressure: Optional[float] = None + # /** In percent */ + humidity: Optional[float] = None + # /** In kelvin */ + dewPoint: Optional[float] = None + forecasts: Optional[list[ForecastData]] = None + hourlyForecasts: Optional[list[HourlyForecastData]] = None + extra_field: Optional[APIUniqueField] = None + immediatePrecipitation: Optional[ImmediatePrecipitation] = None + alerts: Optional[list[AlertData]] = None + + def temp_f(self): + return k_to_f(self.temperature) if self.temperature else None + + def temp_c(self): + return k_to_c(self.temperature) if self.temperature else None + + +class WeatherProvider(ABC): + """ + WeatherProvider tries to emulate the interface specified in cinnamon-spices-applets/weather@mockturtl + such that other providers could be easily ported here in the future + """ + + needsApiKey: bool + prettyName: str + name: Literal["OpenWeatherMap_Open"] # expand in the future + maxForecastSupport: int + maxHourlyForecastSupport: int + website: str + remainingCalls: Optional[int] = None + supportHourlyPrecipChance: bool + supportHourlyPrecipVolume: bool + locationType: Literal["coordinates", "postcode"] + + @abstractmethod + def GetWeather(self, loc: LocationData) -> WeatherData: + pass diff --git a/src/weather.py b/src/weather.py new file mode 100644 index 00000000..8f1b1a2e --- /dev/null +++ b/src/weather.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 +from gi.repository import Gtk, Pango + +from baseWindow import BaseWindow +from floating import Floating +from util import settings, trackers +from util.geojs import GeoJSLocationProvider +from util.location import LocationData +from util.openweathermap import OWMWeatherProvider + +ICON_SIZE = 128 # probably works OK on most screens + + +class WeatherWidget(Floating, BaseWindow): + """ + WeatherWidget displays current weather on screen + + It is a child of the Stage's GtkOverlay, and its placement is + controlled by the overlay's child positioning function. + + When not Awake, it positions itself around all monitors + using a timer which randomizes its halign and valign properties + as well as its current monitor. + """ + + def __init__(self, initial_monitor=0, low_res=False): + super(WeatherWidget, self).__init__(initial_monitor) + self.get_style_context().add_class("weather") + # trying to find a spot that won't overlap with clock or albumArt on init + self.set_halign(Gtk.Align.CENTER) + self.set_valign(Gtk.Align.END) + self.set_property("margin", 6) + + self.low_res = low_res + + if not settings.get_show_weather(): + return + + # overall container + big_box = Gtk.Box(Gtk.Orientation.HORIZONTAL) + self.add(big_box) + big_box.show() + + # icon + self.icon_size = ICON_SIZE + self.condition_icon = Gtk.Image() + self.condition_icon.set_size_request(self.icon_size, self.icon_size) + big_box.pack_start(self.condition_icon, False, False, 6) + self.condition_icon.show() + + # temp + condition + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + big_box.pack_start(box, True, False, 6) + box.show() + + self.temp_label = Gtk.Label() + self.temp_label.show() + self.temp_label.set_line_wrap(True) + self.temp_label.set_alignment(0.5, 0.5) + + box.pack_start(self.temp_label, True, False, 6) + + self.desc_label = Gtk.Label() + self.desc_label.show() + self.desc_label.set_line_wrap(True) + self.desc_label.set_alignment(0.5, 0.5) + + if self.low_res: + self.desc_label.set_max_width_chars(50) + else: + self.desc_label.set_max_width_chars(80) + + box.pack_start(self.desc_label, True, True, 6) + + # TODO: get from settings once other providers are available + self.location_provider = GeoJSLocationProvider() + self.weather_provider = OWMWeatherProvider() + + self.location = self.get_location() + self.update_weather() + + trackers.timer_tracker_get().start_seconds("weather", 600, self.update_weather) + + def get_location(self): + loc_string = settings.get_weather_location() + if loc_string == "" or "," not in loc_string: + return self.location_provider.GetLocation() + lat = float(loc_string.split(",")[0]) + lon = float(loc_string.split(",")[1]) + return LocationData(lat, lon) + + def update_weather(self): + desc_font = Pango.FontDescription.from_string(settings.get_message_font()) + weather_font = Pango.FontDescription.from_string(settings.get_weather_font()) + + if self.low_res: + desc_size = desc_font.get_size() * 0.66 + desc_font.set_size(int(desc_size)) + + weather_data = self.weather_provider.GetWeather(self.location) + + in_str = " " + _("in") + " " + + temp = ( + weather_data.temp_f() + if settings.get_weather_units() == "imperial" + else weather_data.temp_c() + ) + temp_string = str(round(temp)) + desc_message = ( + weather_data.condition.description.title() + + in_str + + weather_data.location.city.capitalize() + ) + + markup = '%s\n ' % ( + desc_font.to_string(), + desc_message, + ) + + self.temp_label.set_markup( + '%s°' % (weather_font.to_string(), temp_string) + ) + self.desc_label.set_markup(markup) + + self.condition_icon.set_from_icon_name( + weather_data.condition.icons[0], Gtk.IconSize.DIALOG + ) + self.condition_icon.set_pixel_size(self.icon_size) + + @staticmethod + def on_destroy(data=None): + trackers.timer_tracker_get().cancel("weather") From d3bcfcc18d285aeee4bcdab08d82ca0c5426aa4c Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sun, 16 Mar 2025 11:20:32 -0400 Subject: [PATCH 2/4] weather.py: Use all possible icon names to construct the weather icon. Not all icon themes have the same set of weather condition icons. The condition metadata can contain multiple icons: icons=['weather-overcast', 'weather-clouds', 'weather-few-clouds'] Providing all of these for the icon lookup improves the chances of a valid match being found (including -symbolic if that's all that is available). --- src/weather.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/weather.py b/src/weather.py index 8f1b1a2e..ec6fbca3 100644 --- a/src/weather.py +++ b/src/weather.py @@ -1,5 +1,5 @@ #!/usr/bin/python3 -from gi.repository import Gtk, Pango +from gi.repository import Gtk, Pango, Gio from baseWindow import BaseWindow from floating import Floating @@ -122,10 +122,8 @@ def update_weather(self): '%s°' % (weather_font.to_string(), temp_string) ) self.desc_label.set_markup(markup) - - self.condition_icon.set_from_icon_name( - weather_data.condition.icons[0], Gtk.IconSize.DIALOG - ) + gicon = Gio.ThemedIcon.new_from_names(weather_data.condition.icons) + self.condition_icon.set_from_gicon(gicon, Gtk.IconSize.DIALOG) self.condition_icon.set_pixel_size(self.icon_size) @staticmethod From bb681ad975c0dd25cdee55d84ee94e78611aa53a Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sun, 16 Mar 2025 15:06:36 -0400 Subject: [PATCH 3/4] Small fixes to accomodate floating widget changes (8137ba2). --- src/stage.py | 3 +-- src/weather.py | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/stage.py b/src/stage.py index 437d50f3..0a43dfe7 100644 --- a/src/stage.py +++ b/src/stage.py @@ -333,7 +333,6 @@ def destroy_children(self): try: if self.weather_widget is not None: - self.weather_widget.stop_positioning() self.weather_widget.destroy() except Exception as e: print(e) @@ -528,7 +527,7 @@ def setup_weather(self): Initially invisible, regardless - its visibility is controlled via its own positioning timer. """ - self.weather_widget = WeatherWidget(None, status.screen.get_mouse_monitor()) + self.weather_widget = WeatherWidget(status.screen.get_mouse_monitor()) self.add_child_widget(self.weather_widget) self.floaters.append(self.weather_widget) diff --git a/src/weather.py b/src/weather.py index ec6fbca3..fd36f41f 100644 --- a/src/weather.py +++ b/src/weather.py @@ -24,11 +24,8 @@ class WeatherWidget(Floating, BaseWindow): """ def __init__(self, initial_monitor=0, low_res=False): - super(WeatherWidget, self).__init__(initial_monitor) + super(WeatherWidget, self).__init__(initial_monitor, Gtk.Align.CENTER, Gtk.Align.START) self.get_style_context().add_class("weather") - # trying to find a spot that won't overlap with clock or albumArt on init - self.set_halign(Gtk.Align.CENTER) - self.set_valign(Gtk.Align.END) self.set_property("margin", 6) self.low_res = low_res From 85bd01b3551d987d17c71129163c616ccb64935c Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sun, 16 Mar 2025 15:06:44 -0400 Subject: [PATCH 4/4] Fix copy/paste oops. --- cinnamon-screensaver.pot | 54 +++++++++++++++++++--------------------- src/stage.py | 2 +- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/cinnamon-screensaver.pot b/cinnamon-screensaver.pot index 2f41a6b2..3fef178b 100644 --- a/cinnamon-screensaver.pot +++ b/cinnamon-screensaver.pot @@ -8,61 +8,61 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-22 04:11-0600\n" +"POT-Creation-Date: 2022-12-02 21:29+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: src/cinnamon-screensaver-command.py:43 +#: src/cinnamon-screensaver-command.py:41 msgid "Causes the screensaver to exit gracefully" msgstr "" -#: src/cinnamon-screensaver-command.py:45 +#: src/cinnamon-screensaver-command.py:43 msgid "Query the state of the screensaver" msgstr "" -#: src/cinnamon-screensaver-command.py:47 +#: src/cinnamon-screensaver-command.py:45 msgid "Query the length of time the screensaver has been active" msgstr "" -#: src/cinnamon-screensaver-command.py:49 +#: src/cinnamon-screensaver-command.py:47 msgid "Tells the running screensaver process to lock the screen immediately" msgstr "" -#: src/cinnamon-screensaver-command.py:51 +#: src/cinnamon-screensaver-command.py:49 msgid "Turn the screensaver on (blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:53 +#: src/cinnamon-screensaver-command.py:51 msgid "If the screensaver is active then deactivate it (un-blank the screen)" msgstr "" -#: src/cinnamon-screensaver-command.py:55 +#: src/cinnamon-screensaver-command.py:53 msgid "Version of this application" msgstr "" -#: src/cinnamon-screensaver-command.py:57 +#: src/cinnamon-screensaver-command.py:55 msgid "Message to be displayed in lock screen" msgstr "" -#: src/cinnamon-screensaver-command.py:106 +#: src/cinnamon-screensaver-command.py:105 msgid "The screensaver is active\n" msgstr "" -#: src/cinnamon-screensaver-command.py:108 +#: src/cinnamon-screensaver-command.py:107 msgid "The screensaver is inactive\n" msgstr "" -#: src/cinnamon-screensaver-command.py:112 +#: src/cinnamon-screensaver-command.py:111 msgid "The screensaver is not currently active.\n" msgstr "" -#: src/cinnamon-screensaver-command.py:114 +#: src/cinnamon-screensaver-command.py:113 #, python-format msgid "The screensaver has been active for %d second.\n" msgid_plural "The screensaver has been active for %d seconds.\n" @@ -80,7 +80,7 @@ msgid "" "prior to this occurring." msgstr "" -#: src/passwordEntry.py:23 src/unlock.py:215 +#: src/passwordEntry.py:23 src/unlock.py:216 msgid "Please enter your password..." msgstr "" @@ -92,56 +92,52 @@ msgstr "" msgid "Switch User" msgstr "" -#: src/unlock.py:188 +#: src/unlock.py:189 msgid "Incorrect password" msgstr "" -#: src/unlock.py:205 +#: src/unlock.py:206 msgid "Checking..." msgstr "" -#: src/unlock.py:249 +#: src/unlock.py:250 msgid "You have the Caps Lock key on." msgstr "" -#: src/weather.py:102 -msgid "in" -msgstr "" - #. This is the first line of text for the backup-locker, explaining how to switch to tty #. and run 'cinnamon-unlock-desktop' command. This appears if the screensaver crashes. -#: backup-locker/cs-backup-locker.c:306 +#: backup-locker/cs-backup-locker.c:255 msgid "Something went wrong with the screensaver." msgstr "" #. (continued) This is a subtitle -#: backup-locker/cs-backup-locker.c:316 +#: backup-locker/cs-backup-locker.c:265 msgid "We'll help you get your desktop back" msgstr "" #. (new section) Bulleted list of steps to take to unlock the desktop; -#: backup-locker/cs-backup-locker.c:327 +#: backup-locker/cs-backup-locker.c:276 #, c-format msgid "Switch to a console using ." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:329 +#: backup-locker/cs-backup-locker.c:278 msgid "Log in by typing your user name followed by your password." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:331 +#: backup-locker/cs-backup-locker.c:280 msgid "At the prompt, type 'cinnamon-unlock-desktop' and press Enter." msgstr "" #. (list continued) -#: backup-locker/cs-backup-locker.c:333 +#: backup-locker/cs-backup-locker.c:282 #, c-format msgid "Switch back to your unlocked desktop using ." msgstr "" #. (end section) Final words after the list of steps -#: backup-locker/cs-backup-locker.c:338 +#: backup-locker/cs-backup-locker.c:287 msgid "If you can reproduce this behavior, please file a report here:" msgstr "" diff --git a/src/stage.py b/src/stage.py index 0a43dfe7..f3c2af57 100644 --- a/src/stage.py +++ b/src/stage.py @@ -532,7 +532,7 @@ def setup_weather(self): self.floaters.append(self.weather_widget) - if settings.get_show_albumart(): + if settings.get_show_weather(): self.weather_widget.start_positioning() def setup_osk(self):