feat: import M3Shapes, M3 loading indicator and weather + resources + media revamp in lockscreen

This commit is contained in:
2026-07-08 22:53:19 +02:00
parent e3b2d33a11
commit 672cb07547
14 changed files with 733 additions and 439 deletions
+31 -3
View File
@@ -1,12 +1,12 @@
pragma Singleton
import QtQml
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import QtQml
import ZShell
import qs.Config
import qs.Components
import qs.Config
Singleton {
id: root
@@ -15,7 +15,24 @@ Singleton {
readonly property list<MprisPlayer> list: Mpris.players.values
property alias manualActive: props.manualActive
function getArtUrl(player: MprisPlayer): string {
if (!player)
return "";
if (player.trackArtUrl)
return player.trackArtUrl;
const url = player.metadata["xesam:url"] ?? "";
if (url.startsWith("https://www.youtube.com/watch")) {
// Fallback for youtube
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
}
return "";
}
function getIdentity(player: MprisPlayer): string {
if (!player)
return "";
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
return alias?.to ?? player.identity;
}
@@ -25,9 +42,12 @@ Singleton {
if (!Config.utilities.toasts.nowPlaying) {
return;
}
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
}
}
target: active
target: root.active
}
PersistentProperties {
@@ -38,8 +58,10 @@ Singleton {
reloadableId: "players"
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Toggle media playback"
// qmllint enable unresolved-type
name: "mediaToggle"
onPressed: {
@@ -49,8 +71,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Previous track"
// qmllint enable unresolved-type
name: "mediaPrev"
onPressed: {
@@ -60,8 +84,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Next track"
// qmllint enable unresolved-type
name: "mediaNext"
onPressed: {
@@ -71,8 +97,10 @@ Singleton {
}
}
// qmllint disable unresolved-type
CustomShortcut {
description: "Stop media playback"
// qmllint enable unresolved-type
name: "mediaStop"
onPressed: root.active?.stop()
+107 -32
View File
@@ -1,7 +1,7 @@
pragma Singleton
import Quickshell
import QtQuick
import Quickshell
import ZShell
import qs.Config
@@ -12,15 +12,15 @@ Singleton {
property var cc
property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
property list<var> forecast
property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
property string loc
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
readonly property string temp: `${cc?.tempC ?? 0}°C`
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string temp: formatTemp(cc?.tempC)
readonly property real windSpeed: cc?.windSpeed ?? 0
function fetchCityFromCoords(coords: string): void {
@@ -29,29 +29,48 @@ Singleton {
return;
}
const [lat, lon] = coords.split(",");
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
Requests.get(url, text => {
const [lat, lon] = coords.split(",").map(s => s.trim());
const lang = Qt.locale().name.split("_")[0] || "en";
const fallbackToBigDataCloud = () => {
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
Requests.get(fallbackUrl, text => {
const geo = JSON.parse(text);
const geoCity = geo.city || geo.locality;
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, city);
} else {
city = "Unknown City";
}
});
};
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
Requests.get(nominatimUrl, text => {
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
if (geo) {
const geoCity = geo.type === "city" ? geo.name : geo.city;
city = geoCity;
cachedCities.set(coords, geoCity);
} else {
city = "Unknown City";
if (geoCity) {
city = fixCityName(geoCity);
cachedCities.set(coords, city);
return;
}
}
});
fallbackToBigDataCloud();
}, fallbackToBigDataCloud);
}
function fetchCoordsFromCity(cityName: string): void {
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
const lang = Qt.locale().name.split("_")[0] || "en";
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
Requests.get(url, text => {
const json = JSON.parse(text);
if (json.results && json.results.length > 0) {
const result = json.results[0];
loc = result.latitude + "," + result.longitude;
city = result.name;
city = fixCityName(result.name);
} else {
loc = "";
reload();
@@ -72,25 +91,21 @@ Singleton {
cc = {
weatherCode: json.current.weather_code,
weatherDesc: getWeatherCondition(json.current.weather_code),
tempC: Math.round(json.current.temperature_2m),
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
feelsLikeC: Math.round(json.current.apparent_temperature),
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
tempC: json.current.temperature_2m,
feelsLikeC: json.current.apparent_temperature,
humidity: json.current.relative_humidity_2m,
windSpeed: json.current.wind_speed_10m,
isDay: json.current.is_day,
sunrise: json.daily.sunrise[0],
sunset: json.daily.sunset[0]
sunrise: json.daily.sunrise[0].replace("T", " "),
sunset: json.daily.sunset[0].replace("T", " ")
};
const forecastList = [];
for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({
date: json.daily.time[i],
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
minTempC: Math.round(json.daily.temperature_2m_min[i]),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
date: json.daily.time[i].replace(/-/g, "/"),
maxTempC: json.daily.temperature_2m_max[i],
minTempC: json.daily.temperature_2m_min[i],
weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
});
@@ -99,7 +114,8 @@ Singleton {
const hourlyList = [];
const now = new Date();
for (let i = 0; i < json.hourly.time.length; i++) {
const time = new Date(json.hourly.time[i]);
const time = new Date(json.hourly.time[i].replace("T", " "));
if (time < now)
continue;
@@ -107,7 +123,7 @@ Singleton {
timestamp: json.hourly.time[i],
hour: time.getHours(),
tempC: Math.round(json.hourly.temperature_2m[i]),
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
precipChance: json.hourly.precipitation_probability[i],
weatherCode: json.hourly.weather_code[i],
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
});
@@ -116,6 +132,59 @@ Singleton {
});
}
function fixCityName(cityName: string): string {
if (!cityName)
return "";
const mapping = {
// Polish
"Poznan": "Poznań",
"Wroclaw": "Wrocław",
"Krakow": "Kraków",
"Gdansk": "Gdańsk",
"Lodz": "Łódź",
"Rzeszow": "Rzeszów",
"Torun": "Toruń",
"Bialystok": "Białystok",
"Czestochowa": "Częstochowa",
"Plock": "Płock",
"Ruda Slaska": "Ruda Śląska",
"Dabrowa Gornicza": "Dąbrowa Górnicza",
"Elblag": "Elbląg",
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
"Zielona Gora": "Zielona Góra",
"Slupsk": "Słupsk",
// German
"Munchen": "München",
"Koln": "Köln",
"Dusseldorf": "Düsseldorf",
"Nurnberg": "Nürnberg",
// French & Spanish & Portuguese
"Sao Paulo": "São Paulo",
"Montreal": "Montréal",
"Quebec": "Québec",
"Bogota": "Bogotá",
"Medellin": "Medellín",
"Cordoba": "Córdoba",
// Turkish
"Istanbul": "İstanbul",
"Izmir": "İzmir",
// Scandinavian & others
"Malmo": "Malmö",
"Goteborg": "Göteborg",
"Zurich": "Zürich",
"Geneve": "Genève"
};
return mapping[cityName] || cityName;
}
function formatTemp(temp: var): string {
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
}
function getWeatherCondition(code: string): string {
const conditions = {
"0": "Clear",
@@ -154,9 +223,9 @@ Singleton {
if (!loc || loc.indexOf(",") === -1)
return "";
const [lat, lon] = loc.split(",");
const [lat, lon] = loc.split(",").map(s => s.trim());
const baseUrl = "https://api.open-meteo.com/v1/forecast";
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
return baseUrl + "?" + params.join("&");
}
@@ -189,7 +258,14 @@ Singleton {
onLocChanged: fetchWeatherData()
// Refresh current location hourly
Connections {
function onWeatherLocationChanged(): void {
root.reload();
}
target: Config.services
}
Timer {
interval: 3600000 // 1 hour
repeat: true
@@ -200,6 +276,5 @@ Singleton {
ElapsedTimer {
id: timer
}
}