Merge branch 'main' into 135-issue-pr-templates
C++ / fmt (pull_request) Successful in 11s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 30s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 57s
Python / typecheck (pull_request) Failing after 52s
C++ / build (pull_request) Successful in 3m4s
Rust / fmt (pull_request) Successful in 1m1s
Rust / build (pull_request) Successful in 1m49s
Python / buildcheck (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m41s
C++ / clang-tidy (pull_request) Successful in 4m41s

This commit is contained in:
2026-07-10 01:32:39 +02:00
15 changed files with 770 additions and 469 deletions
+22 -2
View File
@@ -36,13 +36,15 @@ 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")
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
set(CMAKE_INSTALL_MESSAGE NEVER)
add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
@@ -150,7 +152,6 @@ endif()
if("plugin" IN_LIST ENABLE_MODULES)
add_subdirectory(Plugins)
endif()
if("shell" IN_LIST ENABLE_MODULES)
@@ -167,3 +168,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()
+35 -30
View File
@@ -21,8 +21,6 @@ TextFieldBase {
property bool isError
property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.extraSmall * 2
property real outlineGapScale: activeFocus || text ? 1 : 0
property int radius: Appearance.rounding.small
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Appearance.font.size.small
@@ -57,11 +55,6 @@ TextFieldBase {
onClicked: root.focus = true
}
}
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
onEditingFinished: {
if (!valid)
@@ -103,7 +96,7 @@ TextFieldBase {
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + -Appearance.spacing.extraSmall
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
placeholder.scale: root.smallFontScale
}
@@ -136,7 +129,7 @@ TextFieldBase {
AnchorAnim {
duration: Appearance.anim.durations.expressiveEffects
easing: Appearance.anim.curves.expressiveDefaultEffects
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
}
}
}
@@ -146,12 +139,11 @@ TextFieldBase {
active: root.leadingIcon
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.larger
anchors.leftMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.leadingIcon
}
}
@@ -161,12 +153,11 @@ TextFieldBase {
active: root.trailingIcon
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.larger
anchors.rightMargin: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
text: root.trailingIcon
}
}
@@ -201,12 +192,24 @@ TextFieldBase {
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: path
readonly property real inset: strokeWidth / 2
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.small * 2
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 + root.outlineGap * root.outlineGapScale
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
startY: path.inset
strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
strokeWidth: root.activeFocus ? 2 : 1
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on strokeColor {
CAnim {
}
@@ -217,54 +220,56 @@ TextFieldBase {
}
PathLine {
x: bg.width - root.clampedRadius
x: bg.width - path.inset - root.clampedRadius
y: path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width
y: root.clampedRadius
x: bg.width - path.inset
y: path.inset + root.clampedRadius
}
PathLine {
x: bg.width
y: bg.height - root.clampedRadius
x: bg.width - path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - root.clampedRadius
y: bg.height
x: bg.width - path.inset - root.clampedRadius
y: bg.height - path.inset
}
PathLine {
x: root.clampedRadius
y: bg.height
x: path.inset + root.clampedRadius
y: bg.height - path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: 0
y: bg.height - root.clampedRadius
x: path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathLine {
x: 0
y: root.clampedRadius
x: path.inset
y: path.inset + root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: root.clampedRadius
y: 0
x: path.inset + root.clampedRadius
y: path.inset
}
PathLine {
x: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
y: path.inset
}
}
}
+114
View File
@@ -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();
}
}
}
+48 -18
View File
@@ -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
}
+46
View File
@@ -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
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
}
}
+31 -41
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+62
View File
@@ -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
}
}
+103
View File
@@ -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
View File
@@ -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 {
}
}
}
+25
View File
@@ -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
+10 -1
View File
@@ -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
}
}
}