feat: import M3Shapes, M3 loading indicator and weather + resources + media revamp in lockscreen
This commit is contained in:
+20
-2
@@ -36,7 +36,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(ENABLE_MODULES "plugin;shell" CACHE STRING "Modules to build/install")
|
||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
||||
|
||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||
@@ -150,7 +150,6 @@ endif()
|
||||
|
||||
if("plugin" IN_LIST ENABLE_MODULES)
|
||||
add_subdirectory(Plugins)
|
||||
|
||||
endif()
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
@@ -167,3 +166,22 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Greeter
|
||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||
endif()
|
||||
|
||||
if("m3shapes" IN_LIST ENABLE_MODULES)
|
||||
message(STATUS "Fetching M3Shapes module")
|
||||
include(FetchContent)
|
||||
set(M3SHAPES_REV bdc327b29f95394a732baf3c9b19658ba23755b6)
|
||||
FetchContent_Declare(
|
||||
m3shapes_external
|
||||
GIT_REPOSITORY https://github.com/soramanew/m3shapes.git
|
||||
GIT_TAG ${M3SHAPES_REV}
|
||||
SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/m3shapes-${M3SHAPES_REV}"
|
||||
)
|
||||
FetchContent_MakeAvailable(m3shapes_external)
|
||||
message(STATUS "Done fetching M3Shapes module")
|
||||
|
||||
# Fix m3shapes wrong rpath
|
||||
if(TARGET m3shapesplugin)
|
||||
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import M3Shapes
|
||||
import qs.Config
|
||||
|
||||
MaterialShape {
|
||||
id: root
|
||||
|
||||
property bool animated: true
|
||||
property real cRotation
|
||||
property bool containsIcon
|
||||
property real dampingRatio: 0.6
|
||||
property real lRotation
|
||||
property int morphAnimRotation: 60
|
||||
property real morphScale: 0.14
|
||||
property alias rotateAnimDuration: rotateAnim.duration
|
||||
property int shapeIndex
|
||||
property list<int> shapes: {
|
||||
if (containsIcon)
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
}
|
||||
readonly property real springDuration: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const r = -dampingRatio * wn;
|
||||
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
return Math.log(visibilityThreshold / c) / r;
|
||||
}
|
||||
readonly property real springMaxVelocity: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||
return wn * factor;
|
||||
}
|
||||
property bool springSettled: true
|
||||
property real stiffness: 180
|
||||
property real thisLRotation
|
||||
property real visibilityThreshold: 0.075
|
||||
|
||||
function spring(t: real): var {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const za = dampingRatio * wn;
|
||||
|
||||
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
const r = za / wd;
|
||||
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||
|
||||
return [pos, vel];
|
||||
}
|
||||
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitSize: 38
|
||||
toShape: shapes[0]
|
||||
|
||||
RotationAnimation on cRotation {
|
||||
id: rotateAnim
|
||||
|
||||
duration: 4666
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: root.animated
|
||||
to: 360
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: root.animated && !root.springSettled
|
||||
|
||||
onTriggered: {
|
||||
const t = timer.elapsed();
|
||||
|
||||
if (t >= root.springDuration) {
|
||||
root.springSettled = true;
|
||||
} else {
|
||||
const [pos, vel] = root.spring(t);
|
||||
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||
root.thisLRotation = pos * root.morphAnimRotation;
|
||||
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 650
|
||||
repeat: true
|
||||
running: root.animated
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
root.beginBatchUpdate();
|
||||
root.fromShape = root.toShape;
|
||||
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||
root.toShape = root.shapes[root.shapeIndex];
|
||||
root.morphProgress = 0;
|
||||
|
||||
root.rotation = root.rotation;
|
||||
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||
root.thisLRotation = 0;
|
||||
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||
|
||||
root.springSettled = false;
|
||||
timer.restart();
|
||||
root.endBatchUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,25 +6,55 @@ import qs.Effects
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
||||
return false;
|
||||
|
||||
if (isStart)
|
||||
return visibleArea.yPosition > 0;
|
||||
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
||||
function contentSize(): real {
|
||||
return horizontal ? contentWidth : contentHeight;
|
||||
}
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
orientation: ListView.Vertical
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Behavior on bottomFadeOpacity {
|
||||
if (isStart)
|
||||
return visibleStart() > 0;
|
||||
|
||||
return visibleStart() + visibleRatio() < 1;
|
||||
}
|
||||
|
||||
function marginEnd(): real {
|
||||
return horizontal ? rightMargin : bottomMargin;
|
||||
}
|
||||
|
||||
function marginStart(): real {
|
||||
return horizontal ? leftMargin : topMargin;
|
||||
}
|
||||
|
||||
function overshootStart(): real {
|
||||
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
||||
}
|
||||
|
||||
function viewportSize(): real {
|
||||
return horizontal ? width : height;
|
||||
}
|
||||
|
||||
function visibleRatio(): real {
|
||||
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
||||
}
|
||||
|
||||
function visibleStart(): real {
|
||||
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
||||
}
|
||||
|
||||
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
|
||||
Behavior on endFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
@@ -40,10 +70,10 @@ CustomListView {
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||
position: 0
|
||||
}
|
||||
|
||||
@@ -58,13 +88,13 @@ CustomListView {
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
||||
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on topFadeOpacity {
|
||||
Behavior on startFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Config
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property real amplitude: 3
|
||||
property color color: DynamicColors.palette.m3surfaceContainer
|
||||
readonly property real waveHeight: amplitude * 2
|
||||
property int waves: 4
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
Behavior on fillColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
PathSvg {
|
||||
path: {
|
||||
const w = root.width;
|
||||
const h = root.height;
|
||||
const a = root.amplitude;
|
||||
const n = Math.max(1, root.waves);
|
||||
const wl = w / n;
|
||||
const half = wl / 2;
|
||||
|
||||
let d = `M 0,${a} `;
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const x = i * wl;
|
||||
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
||||
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
||||
}
|
||||
d += `L ${w},${h} L 0,${h} Z`;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-3
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+31
-41
@@ -4,76 +4,66 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
spacing: Appearance.spacing.large * 2
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: weather.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
topLeftRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
WeatherInfo {
|
||||
id: weather
|
||||
|
||||
Layout.fillWidth: true
|
||||
rootHeight: root.height
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: resources.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomLeftRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Media {
|
||||
id: media
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-145
@@ -1,201 +1,110 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: mask
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.2)
|
||||
position: 0.4
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0.8
|
||||
}
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
CustomText {
|
||||
Layout.bottomMargin: Appearance.spacing.larger
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
text: qsTr("Now playing")
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.extraLarge
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackArtist ?? qsTr("No media")
|
||||
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.pointSize: Appearance.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackTitle ?? qsTr("No media")
|
||||
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ButtonRow {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.spacing.large * 1.2
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoPrevious)
|
||||
Players.active.previous();
|
||||
}
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: Players.active?.previous()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canTogglePlaying)
|
||||
Players.active.togglePlaying();
|
||||
}
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
active: Players.active?.isPlaying ?? false
|
||||
animate: true
|
||||
icon: active ? "pause" : "play_arrow"
|
||||
level: active ? 2 : 1
|
||||
set_color: "Primary"
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoNext)
|
||||
Players.active.next();
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext
|
||||
icon: "skip_next"
|
||||
}
|
||||
}
|
||||
}
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
component PlayerControl: CustomRect {
|
||||
id: control
|
||||
|
||||
property bool active
|
||||
property alias animate: controlIcon.animate
|
||||
property alias icon: controlIcon.text
|
||||
property int level: 1
|
||||
property string set_color: "Secondary"
|
||||
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
|
||||
color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
||||
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
|
||||
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: controlState
|
||||
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
|
||||
onClicked: {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: controlIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
fill: control.active ? 1 : 0
|
||||
font.pointSize: Appearance.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
}
|
||||
onClicked: Players.active?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-47
@@ -1,81 +1,82 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Effects
|
||||
|
||||
GridLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
readonly property real fontScale: {
|
||||
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
||||
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
||||
}
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
service: Storage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Resource {
|
||||
id: cpu
|
||||
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3tertiary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: Storage.percentage
|
||||
}
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
component Resource: CircularProgress {
|
||||
id: res
|
||||
|
||||
required property color fgColor
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
implicitSize: width
|
||||
|
||||
Behavior on value {
|
||||
Behavior on clampedVal {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||
fgColor: res.fgColor
|
||||
padding: Appearance.padding.large * 3
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
value: res.value
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.fgColor
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
text: Weather.icon
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: qsTr("Feels like %1").arg(Weather.temp)
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: {
|
||||
const today = Weather.forecast[0];
|
||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||
}
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.largeIncreased
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: "schedule"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: qsTr("Hourly forecast")
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeListView {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
model: Weather.hourlyForecast
|
||||
orientation: VerticalFadeListView.Horizontal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: hour
|
||||
|
||||
readonly property var cond: modelData
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
MaterialShape {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
||||
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
||||
shape: MaterialShape.Cookie4Sided
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: hour.cond.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
text: hour.cond.precipChance + "%"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.extraSmall
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-150
@@ -1,162 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules.Lock.Weather
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
readonly property bool showForecast: rootHeight >= 700
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: -Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
active: root.rootHeight > 610
|
||||
visible: active
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
text: qsTr("Weather")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
||||
text: Weather.icon
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.rightMargin: Appearance.padding.smaller
|
||||
active: root.width > 400
|
||||
visible: active
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecastLoader
|
||||
|
||||
Layout.bottomMargin: Appearance.padding.large * 2
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.smaller
|
||||
active: root.rootHeight > 820
|
||||
visible: active
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const forecast = Weather.hourlyForecast;
|
||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
||||
if (!forecast)
|
||||
return Array.from({
|
||||
length: count
|
||||
}, () => null);
|
||||
|
||||
return forecast.slice(0, count);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastHour
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: {
|
||||
const hour = forecastHour.modelData?.hour ?? 0;
|
||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
||||
font.weight: 500
|
||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: {
|
||||
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||
if (showForecast)
|
||||
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
||||
|
||||
onTriggered: Weather.reload()
|
||||
}
|
||||
|
||||
BriefInfo {
|
||||
id: brief
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Appearance.padding.extraLarge
|
||||
rootHeight: root.rootHeight
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecast
|
||||
|
||||
active: root.showForecast
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Forecast {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,31 @@ Item {
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: img.status === Image.Ready ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CustomRect {
|
||||
color: DynamicColors.palette.m3primaryContainer
|
||||
implicitHeight: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||
implicitWidth: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
LoadingIndicator {
|
||||
id: loadingIndicator
|
||||
|
||||
anchors.centerIn: parent
|
||||
containsIcon: true
|
||||
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: img
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ PageBase {
|
||||
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
|
||||
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
||||
first: true
|
||||
last: true
|
||||
menuItems: root.clockFormats
|
||||
settingAnchor: "bar-clock-format"
|
||||
subtext: qsTr("Change how time is displayed in the widget")
|
||||
@@ -58,5 +57,15 @@ PageBase {
|
||||
Config.general.dateFormat = item.value;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
checked: Config.services.useTwelveHourClock
|
||||
last: true
|
||||
settingAnchor: "bar-clock-twelve-hour"
|
||||
subtext: qsTr("Format timestamps for twelve or twenty-four hours in UI")
|
||||
text: qsTr("Twelve hour clock")
|
||||
|
||||
onToggled: Config.services.useTwelveHourClock = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user