2 Commits
14 changed files with 735 additions and 439 deletions
+22 -2
View File
@@ -36,13 +36,15 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 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_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML 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_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(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
set(CMAKE_INSTALL_MESSAGE NEVER)
add_compile_options( add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion -Wold-style-cast -Wnull-dereference -Wdouble-promotion
@@ -150,7 +152,6 @@ endif()
if("plugin" IN_LIST ENABLE_MODULES) if("plugin" IN_LIST ENABLE_MODULES)
add_subdirectory(Plugins) add_subdirectory(Plugins)
endif() endif()
if("shell" IN_LIST ENABLE_MODULES) if("shell" IN_LIST ENABLE_MODULES)
@@ -167,3 +168,22 @@ if("shell" IN_LIST ENABLE_MODULES)
# Greeter # Greeter
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}") install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
endif() 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()
+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 { CustomListView {
id: root id: root
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property real fadeAmount: 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 { function contentSize(): real {
// When content is smaller than flickable size, hide fade when rebound starts return horizontal ? contentWidth : contentHeight;
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;
} }
flickableDirection: Flickable.VerticalFlick function fadeShouldBeActive(isStart: bool): bool {
layer.enabled: true // When content is smaller than flickable size, hide fade when rebound starts.
orientation: ListView.Vertical 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 { Anim {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
@@ -40,10 +70,10 @@ CustomListView {
visible: false visible: false
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Vertical orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
GradientStop { GradientStop {
color: Qt.rgba(0, 0, 0, root.topFadeOpacity) color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
position: 0 position: 0
} }
@@ -58,13 +88,13 @@ CustomListView {
} }
GradientStop { GradientStop {
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity) color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
position: 1 position: 1
} }
} }
} }
} }
Behavior on topFadeOpacity { Behavior on startFadeOpacity {
Anim { Anim {
type: Anim.SlowEffects 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 pragma Singleton
import QtQml
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import QtQml
import ZShell import ZShell
import qs.Config
import qs.Components import qs.Components
import qs.Config
Singleton { Singleton {
id: root id: root
@@ -15,7 +15,24 @@ Singleton {
readonly property list<MprisPlayer> list: Mpris.players.values readonly property list<MprisPlayer> list: Mpris.players.values
property alias manualActive: props.manualActive 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 { function getIdentity(player: MprisPlayer): string {
if (!player)
return "";
const alias = Config.services.playerAliases.find(a => a.from === player.identity); const alias = Config.services.playerAliases.find(a => a.from === player.identity);
return alias?.to ?? player.identity; return alias?.to ?? player.identity;
} }
@@ -25,9 +42,12 @@ Singleton {
if (!Config.utilities.toasts.nowPlaying) { if (!Config.utilities.toasts.nowPlaying) {
return; 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 { PersistentProperties {
@@ -38,8 +58,10 @@ Singleton {
reloadableId: "players" reloadableId: "players"
} }
// qmllint disable unresolved-type
CustomShortcut { CustomShortcut {
description: "Toggle media playback" description: "Toggle media playback"
// qmllint enable unresolved-type
name: "mediaToggle" name: "mediaToggle"
onPressed: { onPressed: {
@@ -49,8 +71,10 @@ Singleton {
} }
} }
// qmllint disable unresolved-type
CustomShortcut { CustomShortcut {
description: "Previous track" description: "Previous track"
// qmllint enable unresolved-type
name: "mediaPrev" name: "mediaPrev"
onPressed: { onPressed: {
@@ -60,8 +84,10 @@ Singleton {
} }
} }
// qmllint disable unresolved-type
CustomShortcut { CustomShortcut {
description: "Next track" description: "Next track"
// qmllint enable unresolved-type
name: "mediaNext" name: "mediaNext"
onPressed: { onPressed: {
@@ -71,8 +97,10 @@ Singleton {
} }
} }
// qmllint disable unresolved-type
CustomShortcut { CustomShortcut {
description: "Stop media playback" description: "Stop media playback"
// qmllint enable unresolved-type
name: "mediaStop" name: "mediaStop"
onPressed: root.active?.stop() onPressed: root.active?.stop()
+107 -32
View File
@@ -1,7 +1,7 @@
pragma Singleton pragma Singleton
import Quickshell
import QtQuick import QtQuick
import Quickshell
import ZShell import ZShell
import qs.Config import qs.Config
@@ -12,15 +12,15 @@ Singleton {
property var cc property var cc
property string city property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather") 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> forecast
property list<var> hourlyForecast property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0 readonly property int humidity: cc?.humidity ?? 0
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
property string loc property string loc
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--" 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), "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: `${cc?.tempC ?? 0}°C` readonly property string temp: formatTemp(cc?.tempC)
readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0
function fetchCityFromCoords(coords: string): void { function fetchCityFromCoords(coords: string): void {
@@ -29,29 +29,48 @@ Singleton {
return; return;
} }
const [lat, lon] = coords.split(","); const [lat, lon] = coords.split(",").map(s => s.trim());
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; const lang = Qt.locale().name.split("_")[0] || "en";
Requests.get(url, text => {
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; const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
if (geo) { if (geo) {
const geoCity = geo.type === "city" ? geo.name : geo.city; const geoCity = geo.type === "city" ? geo.name : geo.city;
city = geoCity; if (geoCity) {
cachedCities.set(coords, geoCity); city = fixCityName(geoCity);
} else { cachedCities.set(coords, city);
city = "Unknown City"; return;
}
} }
}); fallbackToBigDataCloud();
}, fallbackToBigDataCloud);
} }
function fetchCoordsFromCity(cityName: string): void { 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 => { Requests.get(url, text => {
const json = JSON.parse(text); const json = JSON.parse(text);
if (json.results && json.results.length > 0) { if (json.results && json.results.length > 0) {
const result = json.results[0]; const result = json.results[0];
loc = result.latitude + "," + result.longitude; loc = result.latitude + "," + result.longitude;
city = result.name; city = fixCityName(result.name);
} else { } else {
loc = ""; loc = "";
reload(); reload();
@@ -72,25 +91,21 @@ Singleton {
cc = { cc = {
weatherCode: json.current.weather_code, weatherCode: json.current.weather_code,
weatherDesc: getWeatherCondition(json.current.weather_code), weatherDesc: getWeatherCondition(json.current.weather_code),
tempC: Math.round(json.current.temperature_2m), tempC: json.current.temperature_2m,
tempF: Math.round(toFahrenheit(json.current.temperature_2m)), feelsLikeC: json.current.apparent_temperature,
feelsLikeC: Math.round(json.current.apparent_temperature),
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
humidity: json.current.relative_humidity_2m, humidity: json.current.relative_humidity_2m,
windSpeed: json.current.wind_speed_10m, windSpeed: json.current.wind_speed_10m,
isDay: json.current.is_day, isDay: json.current.is_day,
sunrise: json.daily.sunrise[0], sunrise: json.daily.sunrise[0].replace("T", " "),
sunset: json.daily.sunset[0] sunset: json.daily.sunset[0].replace("T", " ")
}; };
const forecastList = []; const forecastList = [];
for (let i = 0; i < json.daily.time.length; i++) for (let i = 0; i < json.daily.time.length; i++)
forecastList.push({ forecastList.push({
date: json.daily.time[i], date: json.daily.time[i].replace(/-/g, "/"),
maxTempC: Math.round(json.daily.temperature_2m_max[i]), maxTempC: json.daily.temperature_2m_max[i],
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), minTempC: json.daily.temperature_2m_min[i],
minTempC: Math.round(json.daily.temperature_2m_min[i]),
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
weatherCode: json.daily.weather_code[i], weatherCode: json.daily.weather_code[i],
icon: Icons.getWeatherIcon(json.daily.weather_code[i]) icon: Icons.getWeatherIcon(json.daily.weather_code[i])
}); });
@@ -99,7 +114,8 @@ Singleton {
const hourlyList = []; const hourlyList = [];
const now = new Date(); const now = new Date();
for (let i = 0; i < json.hourly.time.length; i++) { 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) if (time < now)
continue; continue;
@@ -107,7 +123,7 @@ Singleton {
timestamp: json.hourly.time[i], timestamp: json.hourly.time[i],
hour: time.getHours(), hour: time.getHours(),
tempC: Math.round(json.hourly.temperature_2m[i]), 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], weatherCode: json.hourly.weather_code[i],
icon: Icons.getWeatherIcon(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 { function getWeatherCondition(code: string): string {
const conditions = { const conditions = {
"0": "Clear", "0": "Clear",
@@ -154,9 +223,9 @@ Singleton {
if (!loc || loc.indexOf(",") === -1) if (!loc || loc.indexOf(",") === -1)
return ""; 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 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("&"); return baseUrl + "?" + params.join("&");
} }
@@ -189,7 +258,14 @@ Singleton {
onLocChanged: fetchWeatherData() onLocChanged: fetchWeatherData()
// Refresh current location hourly Connections {
function onWeatherLocationChanged(): void {
root.reload();
}
target: Config.services
}
Timer { Timer {
interval: 3600000 // 1 hour interval: 3600000 // 1 hour
repeat: true repeat: true
@@ -200,6 +276,5 @@ Singleton {
ElapsedTimer { ElapsedTimer {
id: timer id: timer
} }
} }
+31 -41
View File
@@ -4,76 +4,66 @@ import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
RowLayout { CustomClippingRect {
id: root id: root
required property var lock required property var lock
spacing: Appearance.spacing.large * 2 implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
radius: Appearance.rounding.large
ColumnLayout { RowLayout {
Layout.fillWidth: true id: layout
spacing: Appearance.spacing.normal
CustomRect { anchors.fill: parent
spacing: Appearance.spacing.large * 2
ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer spacing: Appearance.spacing.normal
implicitHeight: weather.implicitHeight
radius: Appearance.rounding.small
topLeftRadius: Appearance.rounding.large
WeatherInfo { WeatherInfo {
id: weather id: weather
Layout.fillWidth: true
rootHeight: root.height rootHeight: root.height
} }
}
CustomRect {
Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: resources.implicitHeight
radius: Appearance.rounding.small
Resources { Resources {
id: 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 { Media {
id: media id: media
Layout.fillHeight: true
Layout.fillWidth: true
lock: root.lock lock: root.lock
} }
} }
}
Center { Center {
lock: root.lock lock: root.lock
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
bottomRightRadius: Appearance.rounding.large spacing: Appearance.spacing.normal
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.small
topRightRadius: Appearance.rounding.large
NotifDock { CustomRect {
lock: root.lock 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
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules import Quickshell
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers
import qs.Config import qs.Config
import qs.Helpers
Item { CustomClippingRect {
id: root id: root
required property var lock 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 anchors.fill: parent
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
layer.enabled: true layer.enabled: true
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
source: Players.active?.trackArtUrl ?? "" source: Players.getArtUrl(Players.active)
sourceSize.height: height sourceSize: {
sourceSize.width: width const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
layer.effect: OpacityMask {
maskSource: mask
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: Appearance.anim.durations.extraLarge type: Anim.StandardExtraLarge
} }
} }
}
Rectangle { CustomRect {
id: mask anchors.fill: parent
color: DynamicColors.palette.m3surface
anchors.fill: parent opacity: 0.7
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
}
} }
} }
ColumnLayout { ColumnLayout {
id: layout id: layout
anchors.fill: parent anchors.left: parent.left
anchors.margins: Appearance.padding.large anchors.margins: Appearance.padding.extraLarge
anchors.right: parent.right
CustomText { anchors.verticalCenter: parent.verticalCenter
Layout.bottomMargin: Appearance.spacing.larger spacing: Appearance.spacing.extraSmall
Layout.topMargin: Appearance.padding.large
color: DynamicColors.palette.m3onSurfaceVariant
font.family: Appearance.font.family.mono
font.weight: 500
text: qsTr("Now playing")
}
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
animate: true animate: true
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
elide: Text.ElideRight elide: Text.ElideRight
font.family: Appearance.font.family.mono font.pointSize: Appearance.font.size.medium
font.pointSize: Appearance.font.size.large
font.weight: 600
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: Players.active?.trackArtist ?? qsTr("No media") text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
} }
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
animate: true animate: true
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight elide: Text.ElideRight
font.family: Appearance.font.family.mono font.pointSize: Appearance.font.size.small
font.pointSize: Appearance.font.size.larger
horizontalAlignment: Text.AlignHCenter 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.alignment: Qt.AlignHCenter
Layout.bottomMargin: Appearance.padding.large Layout.topMargin: Appearance.spacing.small
Layout.topMargin: Appearance.spacing.large * 1.2 spacing: Appearance.spacing.extraSmall
spacing: Appearance.spacing.large
PlayerControl {
function onClicked(): void {
if (Players.active?.canGoPrevious)
Players.active.previous();
}
IconButton {
enabled: Players.active?.canGoPrevious
icon: "skip_previous" icon: "skip_previous"
isRound: true
shapeMorph: true
type: IconButton.Tonal
onClicked: Players.active?.previous()
} }
PlayerControl { IconButton {
function onClicked(): void { checked: Players.active?.isPlaying ?? false
if (Players.active?.canTogglePlaying) enabled: Players.active?.canTogglePlaying
Players.active.togglePlaying(); icon: Players.active?.isPlaying ? "pause" : "play_arrow"
} implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
isRound: true
shapeMorph: true
active: Players.active?.isPlaying ?? false onClicked: Players.active?.togglePlaying()
animate: true
icon: active ? "pause" : "play_arrow"
level: active ? 2 : 1
set_color: "Primary"
} }
PlayerControl { IconButton {
function onClicked(): void { enabled: Players.active?.canGoNext
if (Players.active?.canGoNext)
Players.active.next();
}
icon: "skip_next" icon: "skip_next"
} isRound: true
} shapeMorph: true
} type: IconButton.Tonal
component PlayerControl: CustomRect { onClicked: Players.active?.next()
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 {
}
} }
} }
} }
+48 -47
View File
@@ -1,81 +1,82 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import M3Shapes
import ZShell.Services import ZShell.Services
import qs.Components import qs.Components
import qs.Helpers
import qs.Config import qs.Config
import qs.Effects
GridLayout { CustomRect {
id: root id: root
anchors.left: parent.left readonly property real fontScale: {
anchors.margins: Appearance.padding.large const diff = width / 391 - 1; // 391 is the width at 1080 height screen
anchors.right: parent.right return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
columnSpacing: Appearance.spacing.large }
columns: 2
rowSpacing: Appearance.spacing.large color: DynamicColors.tPalette.m3surfaceContainer
rows: 1 implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Appearance.rounding.small
ServiceRef {
service: Cpu
}
ServiceRef { ServiceRef {
service: Memory service: Memory
} }
ServiceRef { ServiceRef {
service: Cpu service: Storage
} }
Resource { RowLayout {
Layout.bottomMargin: Appearance.padding.large id: layout
Layout.topMargin: Appearance.padding.large
fgColor: DynamicColors.palette.m3primary anchors.fill: parent
icon: "memory" anchors.margins: Appearance.padding.large
value: Cpu.percentage 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 { component Resource: CircularProgress {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: Memory.percentage
}
component Resource: CustomRect {
id: res id: res
required property color fgColor
required property string icon required property string icon
required property real value
Layout.fillWidth: true Layout.fillWidth: true
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitSize: width
implicitHeight: width
radius: Appearance.rounding.large
Behavior on value { Behavior on clampedVal {
Anim { 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 { MaterialIcon {
id: icon
anchors.centerIn: parent anchors.centerIn: parent
color: res.fgColor color: res.fgColor
font.pointSize: (circ.arcRadius * 0.7) || 1 font.pointSize: Appearance.font.size.extraLarge
font.weight: 600
text: res.icon 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
import QtQuick.Layouts import qs.Modules.Lock.Weather
import qs.Config
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config
ColumnLayout { CustomRect {
id: root id: root
required property int rootHeight required property int rootHeight
readonly property bool showForecast: rootHeight >= 700
anchors.left: parent.left color: DynamicColors.tPalette.m3surfaceContainer
anchors.margins: Appearance.padding.large * 2 implicitHeight: {
anchors.right: parent.right const base = brief.implicitHeight + brief.anchors.topMargin;
spacing: Appearance.spacing.small if (showForecast)
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
Loader { return base + brief.anchors.topMargin;
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`
}
}
}
}
} }
radius: Appearance.rounding.small
Timer { Timer {
interval: 900000 // 15 minutes interval: 900000 // 15 minutes
@@ -166,4 +27,27 @@ ColumnLayout {
onTriggered: Weather.reload() 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 implicitHeight: width
radius: Appearance.rounding.large 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 { Image {
id: img id: img
+10 -1
View File
@@ -48,7 +48,6 @@ PageBase {
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
active: root.clockFormats.find(item => item.value === Config.general.dateFormat) active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
first: true first: true
last: true
menuItems: root.clockFormats menuItems: root.clockFormats
settingAnchor: "bar-clock-format" settingAnchor: "bar-clock-format"
subtext: qsTr("Change how time is displayed in the widget") subtext: qsTr("Change how time is displayed in the widget")
@@ -58,5 +57,15 @@ PageBase {
Config.general.dateFormat = item.value; 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
}
} }
} }