add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,119 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
DraggableDesktopWidget {
id: root
defaultY: 280
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["AudioVisualizer"]
readonly property int visualizerWidth: (widgetData && widgetData.width !== undefined) ? widgetData.width : (widgetMetadata?.width ?? 320)
readonly property int visualizerHeight: (widgetData && widgetData.height !== undefined) ? widgetData.height : (widgetMetadata?.height ?? 72)
readonly property string visualizerType: (widgetData && widgetData.visualizerType !== undefined) ? widgetData.visualizerType : (widgetMetadata?.visualizerType ?? "linear")
readonly property bool hideWhenIdle: (widgetData && widgetData.hideWhenIdle !== undefined) ? widgetData.hideWhenIdle : (widgetMetadata?.hideWhenIdle ?? false)
readonly property string colorName: (widgetData && widgetData.colorName !== undefined) ? widgetData.colorName : (widgetMetadata?.colorName ?? "primary")
readonly property color fillColor: Color.resolveColorKey(colorName)
readonly property bool shouldShow: visualizerType !== "" && visualizerType !== "none" && (!hideWhenIdle || MediaService.isPlaying)
readonly property bool isHidden: !shouldShow
readonly property bool shouldRegisterSpectrum: shouldShow
// Keep widget visible in edit mode so users can move/configure it
visible: !root.isHidden || DesktopWidgetRegistry.editMode
readonly property string spectrumComponentId: "desktop:audiovisualizer:" + (root.screen ? root.screen.name : "unknown") + ":" + root.widgetIndex
onShouldRegisterSpectrumChanged: {
if (root.shouldRegisterSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
} else {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
}
Component.onCompleted: {
if (root.shouldRegisterSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
}
}
Component.onDestruction: {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
implicitWidth: Math.round(visualizerWidth * widgetScale)
implicitHeight: Math.round(visualizerHeight * widgetScale)
width: implicitWidth
height: implicitHeight
Rectangle {
id: visualizerMask
anchors.fill: parent
color: "transparent"
radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0
clip: true
Loader {
id: visualizerLoader
anchors.fill: parent
anchors.margins: root.showBackground ? Math.round(Style.marginXS * root.widgetScale) : 0
active: root.shouldShow
asynchronous: true
sourceComponent: {
switch (root.visualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
}
}
}
}
Component {
id: linearComponent
NLinearSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: mirroredComponent
NMirroredSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: waveComponent
NWaveSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: root.fillColor
showMinimumSignal: true
mirrored: Settings.data.audio.spectrumMirrored
}
}
}
@@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.UI
import qs.Widgets
DraggableDesktopWidget {
id: root
readonly property var now: Time.now
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["Clock"]
readonly property color clockTextColor: Color.resolveColorKey(clockColor)
readonly property real fontSize: Math.round(Style.fontSizeXXXL * 2.5 * widgetScale)
readonly property real widgetOpacity: widgetData.opacity !== undefined ? widgetData.opacity : 1.0
readonly property string clockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle
readonly property string clockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor
readonly property bool useCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont
readonly property string customFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont
readonly property string format: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format
readonly property real contentPadding: Math.round((clockStyle === "minimal" ? Style.marginL : Style.marginXL) * widgetScale)
implicitWidth: contentLoader.item ? Math.round((contentLoader.item.implicitWidth || contentLoader.item.width || 0) + contentPadding * 2) : 0
implicitHeight: contentLoader.item ? Math.round((contentLoader.item.implicitHeight || contentLoader.item.height || 0) + contentPadding * 2) : 0
width: implicitWidth
height: implicitHeight
Component {
id: nclockComponent
NClock {
now: root.now
clockStyle: root.clockStyle
backgroundColor: "transparent"
clockColor: clockTextColor
progressColor: Color.mPrimary
opacity: root.widgetOpacity
height: Math.round(fontSize * 1.9)
width: height
hoursFontSize: fontSize * 0.6
minutesFontSize: fontSize * 0.4
scaleRatio: root.widgetScale
}
}
Component {
id: minimalClockComponent
ColumnLayout {
spacing: -2
opacity: root.widgetOpacity
Repeater {
model: I18n.locale.toString(root.now, root.format.trim()).split("\\n")
delegate: NText {
visible: text !== ""
text: modelData
family: root.useCustomFont && root.customFont ? root.customFont : Settings.data.ui.fontDefault
pointSize: {
if (model.length == 1) {
return Math.round(Style.fontSizeXXL * root.widgetScale);
} else {
return Math.round((index == 0) ? Style.fontSizeXXL * root.widgetScale : Style.fontSizeM * root.widgetScale);
}
}
font.weight: Style.fontWeightBold
color: root.clockTextColor
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
}
}
}
Loader {
id: contentLoader
anchors.centerIn: parent
z: 2
sourceComponent: clockStyle === "minimal" ? minimalClockComponent : nclockComponent
}
}
@@ -0,0 +1,311 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Media
import qs.Services.UI
import qs.Widgets
import qs.Widgets.AudioSpectrum
DraggableDesktopWidget {
id: root
defaultY: 200
// Widget settings - check widgetData exists before accessing properties
readonly property string hideMode: (widgetData && widgetData.hideMode !== undefined) ? widgetData.hideMode : "visible"
readonly property bool showButtons: (widgetData && widgetData.showButtons !== undefined) ? widgetData.showButtons : true
readonly property bool showAlbumArt: (widgetData && widgetData.showAlbumArt !== undefined) ? widgetData.showAlbumArt : true
readonly property bool showVisualizer: (widgetData && widgetData.showVisualizer !== undefined) ? widgetData.showVisualizer : true
readonly property string visualizerType: (widgetData && widgetData.visualizerType && widgetData.visualizerType !== "") ? widgetData.visualizerType : "linear"
readonly property bool roundedCorners: (widgetData && widgetData.roundedCorners !== undefined) ? widgetData.roundedCorners : true
readonly property bool hasPlayer: MediaService.currentPlayer !== null
readonly property bool isPlaying: MediaService.isPlaying
readonly property bool hasActiveTrack: hasPlayer && (MediaService.trackTitle || MediaService.trackArtist)
// State
// Hide when idle when playback is not active
readonly property bool shouldHideIdle: (hideMode === "idle") && !isPlaying
readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden"
readonly property bool isHidden: (shouldHideIdle || shouldHideEmpty) && !DesktopWidgetRegistry.editMode
visible: !isHidden
// SpectrumService registration for visualizer
readonly property string spectrumComponentId: "desktopmediaplayer:" + (root.screen ? root.screen.name : "unknown")
readonly property bool needsSpectrum: root.shouldShowVisualizer && !root.isHidden
onNeedsSpectrumChanged: {
if (root.needsSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
} else {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
}
Component.onCompleted: {
if (root.needsSpectrum) {
SpectrumService.registerComponent(root.spectrumComponentId);
}
}
Component.onDestruction: {
SpectrumService.unregisterComponent(root.spectrumComponentId);
}
readonly property bool showPrev: hasPlayer && MediaService.canGoPrevious
readonly property bool showNext: hasPlayer && MediaService.canGoNext
readonly property int visibleButtonCount: root.showButtons ? (1 + (showPrev ? 1 : 0) + (showNext ? 1 : 0)) : 0
implicitWidth: Math.round(400 * widgetScale)
implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale)
width: implicitWidth
height: implicitHeight
// Visualizer visibility mode
readonly property bool shouldShowVisualizer: {
if (!root.showVisualizer)
return false;
if (root.visualizerType === "" || root.visualizerType === "none")
return false;
return true;
}
// Visualizer overlay (visibility controlled by visualizerVisibility setting)
// Completely disabled during scaling to avoid expensive canvas redraws
Loader {
anchors.fill: parent
anchors.leftMargin: Math.round(Style.marginXS * widgetScale)
anchors.rightMargin: Math.round(Style.marginXS * widgetScale)
anchors.topMargin: Math.round(Style.marginXS * widgetScale)
anchors.bottomMargin: 0
z: 0
clip: true
active: needsSpectrum
layer.enabled: true
layer.smooth: true
layer.effect: MultiEffect {
maskEnabled: root.roundedCorners
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: root.width - Math.round(Style.marginXS * widgetScale) * 2
height: root.height - Math.round(Style.marginXS * widgetScale)
radius: root.roundedCorners ? Math.round(Math.max(0, (Style.radiusL - Style.marginXS) * widgetScale)) : 0
color: "white"
}
}
}
sourceComponent: {
switch (root.visualizerType) {
case "linear":
return linearComponent;
case "mirrored":
return mirroredComponent;
case "wave":
return waveComponent;
default:
return null;
}
}
Component {
id: linearComponent
NLinearSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.5
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: mirroredComponent
NMirroredSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.5
mirrored: Settings.data.audio.spectrumMirrored
}
}
Component {
id: waveComponent
NWaveSpectrum {
anchors.fill: parent
values: SpectrumService.values
fillColor: Color.mPrimary
opacity: 0.5
mirrored: Settings.data.audio.spectrumMirrored
}
}
}
// Drop shadow for text and controls readability over visualizer
// Disabled during scaling to avoid expensive recomputations
NDropShadow {
visible: !root.isScaling
anchors.fill: contentLayout
source: contentLayout
z: 1
autoPaddingEnabled: true
shadowBlur: 1.0
shadowOpacity: 0.9
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
}
RowLayout {
id: contentLayout
states: [
State {
when: root.showButtons
AnchorChanges {
target: contentLayout
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
},
State {
when: !root.showButtons
AnchorChanges {
target: contentLayout
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.top: undefined
anchors.bottom: undefined
anchors.left: undefined
anchors.right: undefined
}
}
]
anchors.margins: Math.round(Style.marginM * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
z: 2
Item {
visible: root.showAlbumArt
Layout.preferredWidth: Math.round(48 * widgetScale)
Layout.preferredHeight: Math.round(48 * widgetScale)
Layout.alignment: Qt.AlignVCenter
NImageRounded {
visible: hasPlayer
anchors.fill: parent
radius: Math.round(Style.radiusM * widgetScale)
imagePath: MediaService.trackArtUrl
imageFillMode: Image.PreserveAspectCrop
fallbackIcon: isPlaying ? "media-pause" : "media-play"
fallbackIconSize: Math.round(20 * widgetScale)
borderWidth: 0
}
NIcon {
visible: !hasPlayer
anchors.centerIn: parent
icon: "disc"
pointSize: Math.round(24 * widgetScale)
color: Color.mOnSurfaceVariant
}
}
ColumnLayout {
visible: root.showAlbumArt
Layout.fillWidth: true
Layout.alignment: root.showButtons ? Qt.AlignVCenter : Qt.AlignCenter
spacing: 0
NText {
Layout.fillWidth: true
text: hasPlayer ? (MediaService.trackTitle || "Unknown Track") : "No media playing"
pointSize: Math.round(Style.fontSizeS * widgetScale)
font.weight: Style.fontWeightSemiBold
color: Color.mOnSurface
elide: Text.ElideRight
maximumLineCount: 1
}
NText {
visible: hasPlayer && MediaService.trackArtist
Layout.fillWidth: true
text: MediaService.trackArtist || ""
pointSize: Math.round(Style.fontSizeXS * widgetScale)
font.weight: Style.fontWeightRegular
color: Color.mSecondary
elide: Text.ElideRight
maximumLineCount: 1
}
}
RowLayout {
id: controlsRow
spacing: Math.round(Style.marginXS * widgetScale)
z: 10
visible: root.showButtons
Layout.alignment: root.showAlbumArt ? Qt.AlignVCenter : Qt.AlignCenter
NIconButton {
opacity: showPrev ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutQuad
}
}
baseSize: Math.round(32 * widgetScale)
icon: "media-prev"
enabled: hasPlayer && MediaService.canGoPrevious
colorBg: Color.mSurfaceVariant
colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant
customRadius: Math.round(Style.radiusS * widgetScale)
onClicked: {
if (enabled)
MediaService.previous();
}
}
NIconButton {
baseSize: Math.round(36 * widgetScale)
icon: isPlaying ? "media-pause" : "media-play"
enabled: hasPlayer && (MediaService.canPlay || MediaService.canPause)
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBgHover: Qt.lighter(Color.mPrimary, 1.1)
colorFgHover: Color.mOnPrimary
customRadius: Math.round(Style.radiusS * widgetScale)
onClicked: {
if (enabled) {
MediaService.playPause();
}
}
}
NIconButton {
opacity: showNext ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutQuad
}
}
baseSize: Math.round(32 * widgetScale)
icon: "media-next"
enabled: hasPlayer && MediaService.canGoNext
colorBg: Color.mSurfaceVariant
colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant
customRadius: Math.round(Style.radiusS * widgetScale)
onClicked: {
if (enabled)
MediaService.next();
}
}
}
}
}
@@ -0,0 +1,305 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.System
import qs.Services.UI
import qs.Widgets
DraggableDesktopWidget {
id: root
// Widget settings
readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["SystemStat"]
readonly property string statType: (widgetData && widgetData.statType !== undefined) ? widgetData.statType : (widgetMetadata.statType !== undefined ? widgetMetadata.statType : "CPU")
readonly property string diskPath: (widgetData && widgetData.diskPath !== undefined) ? widgetData.diskPath : "/"
readonly property string layout: (widgetData && widgetData.layout !== undefined) ? widgetData.layout : (widgetMetadata.layout !== undefined ? widgetMetadata.layout : "side")
// Fixed colors
readonly property color color: Color.mPrimary
readonly property color color2: Color.mSecondary
// Legend items model - each item has: text, color, icon (optional), bold (optional), opacity (optional), elide (optional)
readonly property var legendItems: {
switch (root.statType) {
case "CPU":
return [
{
icon: "cpu-usage",
text: Math.round(SystemStatService.cpuUsage) + "%",
color: root.color
},
{
text: "cpu-usage",
text: SystemStatService.cpuFreq,
color: root.color,
opacity: 0.8
},
{
icon: "cpu-temperature",
text: SystemStatService.cpuTemp + "°C",
color: root.color2
}
];
case "GPU":
return [
{
icon: "gpu-temperature",
text: Math.round(SystemStatService.gpuTemp) + "°C",
color: root.color
}
];
case "Memory":
return [
{
icon: "memory",
text: Math.round(SystemStatService.memPercent) + "%",
color: root.color
}
];
case "Disk":
var items = [
{
icon: "storage",
text: Math.round(SystemStatService.diskPercents[root.diskPath] || 0) + "%",
color: root.color
}
];
if (root.diskPath !== "/") {
items.push({
text: root.diskPath,
color: root.color,
opacity: 0.8,
elide: true
});
}
return items;
case "Network":
return [
{
icon: "download-speed",
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed),
color: root.color
},
{
icon: "upload-speed",
text: SystemStatService.formatSpeed(SystemStatService.txSpeed),
color: root.color2
}
];
default:
return [];
}
}
// History from service
readonly property var history: {
switch (root.statType) {
case "CPU":
return SystemStatService.cpuHistory;
case "GPU":
return SystemStatService.gpuTempHistory;
case "Memory":
return SystemStatService.memHistory;
case "Disk":
return SystemStatService.diskHistories[root.diskPath] || [];
case "Network":
return SystemStatService.rxSpeedHistory;
default:
return [];
}
}
// Secondary history (CPU temp for CPU, Tx for Network)
readonly property var history2: {
switch (root.statType) {
case "CPU":
return SystemStatService.cpuTempHistory;
case "Network":
return SystemStatService.txSpeedHistory;
default:
return [];
}
}
// Graph min/max values
readonly property real graphMinValue: root.statType === "GPU" ? Math.max(SystemStatService.gpuTempHistoryMin - 5, 0) : 0
readonly property real graphMaxValue: {
switch (root.statType) {
case "CPU":
case "Memory":
case "Disk":
return 100; // Percentage-based stats use fixed 0-100 range
case "GPU":
return Math.max(SystemStatService.gpuTempHistoryMax + 5, 1);
case "Network":
return SystemStatService.rxMaxSpeed;
default:
return 100;
}
}
readonly property real graphMinValue2: {
switch (root.statType) {
case "CPU":
return Math.max(SystemStatService.cpuTempHistoryMin - 5, 0);
default:
return graphMinValue;
}
}
readonly property real graphMaxValue2: {
switch (root.statType) {
case "CPU":
return Math.max(SystemStatService.cpuTempHistoryMax + 5, 1);
case "Network":
return SystemStatService.txMaxSpeed;
default:
return graphMaxValue;
}
}
Component.onCompleted: SystemStatService.registerComponent("desktop-sysstat:" + root.statType)
Component.onDestruction: SystemStatService.unregisterComponent("desktop-sysstat:" + root.statType)
implicitWidth: Math.round(240 * widgetScale)
implicitHeight: Math.round(120 * widgetScale)
width: implicitWidth
height: implicitHeight
// Update interval per stat type
readonly property int graphUpdateInterval: {
switch (root.statType) {
case "CPU":
return SystemStatService.cpuUsageIntervalMs;
case "GPU":
return SystemStatService.gpuIntervalMs;
case "Memory":
return SystemStatService.memIntervalMs;
case "Disk":
return SystemStatService.diskIntervalMs;
case "Network":
return SystemStatService.networkIntervalMs;
default:
return 1000;
}
}
// Graph component (shared between layouts)
Component {
id: graphComponent
NGraph {
values: root.history
values2: root.history2
minValue: root.graphMinValue
maxValue: root.graphMaxValue
minValue2: root.graphMinValue2
maxValue2: root.graphMaxValue2
color: root.color
color2: root.color2
fill: true
updateInterval: root.graphUpdateInterval
strokeWidth: Math.max(1, root.widgetScale)
animateScale: root.statType === "Network"
}
}
// Side layout: icon + legend on left, graph on right
RowLayout {
anchors.fill: parent
anchors.margins: Math.round(Style.marginM * widgetScale)
spacing: Math.round(Style.marginL * widgetScale)
visible: root.layout === "side"
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
Layout.preferredWidth: Math.round(64 * widgetScale)
spacing: Style.marginXS * root.widgetScale
Repeater {
model: root.legendItems
delegate: RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Math.round(Style.marginXXS * root.widgetScale)
NIcon {
visible: !!modelData.icon
icon: modelData.icon || ""
color: modelData.color
pointSize: Style.fontSizeS * root.widgetScale
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
}
NText {
text: modelData.text
color: modelData.color
pointSize: Style.fontSizeS * root.widgetScale
font.family: Settings.data.ui.fontFixed
font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone
Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1
horizontalAlignment: Text.AlignHCenter
}
}
}
}
Loader {
active: root.layout === "side"
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: graphComponent
}
}
// Bottom layout: full-width graph, horizontal legend at bottom
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.round(Style.marginM * widgetScale)
spacing: Math.round(Style.marginS * widgetScale)
visible: root.layout === "bottom"
Loader {
active: root.layout === "bottom"
Layout.fillWidth: true
Layout.fillHeight: true
sourceComponent: graphComponent
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Math.round(Style.marginM * widgetScale)
Repeater {
model: root.legendItems
delegate: RowLayout {
Layout.alignment: Qt.AlignVCenter
spacing: Math.round(Style.marginXXS * root.widgetScale)
NIcon {
Layout.alignment: Qt.AlignVCenter
visible: !!modelData.icon
icon: modelData.icon || ""
color: modelData.color
pointSize: Style.fontSizeS * root.widgetScale
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
}
NText {
Layout.alignment: Qt.AlignVCenter
text: modelData.text
color: modelData.color
pointSize: Style.fontSizeS * root.widgetScale
font.family: Settings.data.ui.fontFixed
font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular
opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0
elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone
Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1
}
}
}
}
}
}
@@ -0,0 +1,146 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Location
import qs.Widgets
DraggableDesktopWidget {
id: root
readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null)
readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0
readonly property real currentTemp: {
if (!weatherReady)
return 0;
var temp = LocationService.data.weather.current_weather.temperature;
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp);
}
return Math.round(temp);
}
readonly property real todayMax: {
if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_max.length === 0)
return 0;
var temp = LocationService.data.weather.daily.temperature_2m_max[0];
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp);
}
return Math.round(temp);
}
readonly property real todayMin: {
if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_min.length === 0)
return 0;
var temp = LocationService.data.weather.daily.temperature_2m_min[0];
if (Settings.data.location.useFahrenheit) {
temp = LocationService.celsiusToFahrenheit(temp);
}
return Math.round(temp);
}
readonly property string tempUnit: Settings.data.location.useFahrenheit ? "F" : "C"
readonly property string locationName: {
const chunks = Settings.data.location.name.split(",");
return chunks[0];
}
implicitWidth: Math.round(Math.max(240 * widgetScale, contentLayout.implicitWidth + Style.margin2M * widgetScale))
implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale)
width: implicitWidth
height: implicitHeight
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: Math.round(Style.marginM * widgetScale)
spacing: Math.round(Style.marginM * widgetScale)
z: 2
Item {
Layout.preferredWidth: Math.round(64 * widgetScale)
Layout.preferredHeight: Math.round(64 * widgetScale)
Layout.alignment: Qt.AlignVCenter
NIcon {
visible: !LocationService.taliaWeatherMascotActive || !weatherReady
anchors.centerIn: parent
icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode) : (LocationService.locationConfigured ? "weather-cloud-off" : "map-pin-off")
pointSize: Math.round(Style.fontSizeXXXL * 2 * widgetScale)
color: weatherReady ? Color.mPrimary : Color.mOnSurfaceVariant
}
Loader {
active: LocationService.taliaWeatherMascotActive && weatherReady
anchors.fill: parent
asynchronous: true
sourceComponent: Component {
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectFit
smooth: true
mipmap: true
asynchronous: true
source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(currentWeatherCode))
}
}
}
}
NText {
text: weatherReady ? `${currentTemp}°${tempUnit}` : "--"
pointSize: Math.round(Style.fontSizeXXXL * widgetScale)
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout {
Layout.fillWidth: true
spacing: Math.round(Style.marginXXS * widgetScale)
Layout.alignment: Qt.AlignVCenter
NText {
Layout.fillWidth: true
text: locationName || I18n.tr("common.weather-no-location")
pointSize: Math.round(Style.fontSizeS * widgetScale)
font.weight: Style.fontWeightRegular
color: Color.mOnSurfaceVariant
elide: Text.ElideRight
maximumLineCount: 1
visible: !Settings.data.location.hideWeatherCityName
}
RowLayout {
spacing: Math.round(Style.marginXS * widgetScale)
visible: weatherReady && todayMax > 0 && todayMin > 0
NText {
text: "H:"
pointSize: Math.round(Style.fontSizeXS * widgetScale)
color: Color.mOnSurfaceVariant
}
NText {
text: `${todayMax}°`
pointSize: Math.round(Style.fontSizeXS * widgetScale)
color: Color.mOnSurface
}
NText {
text: "•"
pointSize: Math.round(Style.fontSizeXXS * widgetScale)
color: Color.mOnSurfaceVariant
opacity: 0.5
}
NText {
text: "L:"
pointSize: Math.round(Style.fontSizeXS * widgetScale)
color: Color.mOnSurfaceVariant
}
NText {
text: `${todayMin}°`
pointSize: Math.round(Style.fontSizeXS * widgetScale)
color: Color.mOnSurfaceVariant
}
}
}
}
}