feat: init quickshell

This commit is contained in:
2025-07-13 22:10:17 +08:00
parent a63be876f7
commit 237a62ea8a
103 changed files with 14997 additions and 498 deletions

View File

@@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar button
Rectangle {
id: calendarButton
width: 40
height: 80
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: calendarMouseArea.containsMouse
property bool calendarVisible: false
property var calendarPopup: null
property var shell: null // Shell reference from parent
signal entered
signal exited
// Hover state management
onContainsMouseChanged: {
if (containsMouse) {
entered();
} else {
exited();
}
}
MouseArea {
id: calendarMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
toggleCalendar();
}
}
// Calendar icon
Label {
anchors.centerIn: parent
text: "calendar_month"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
// Toggle calendar popup
function toggleCalendar() {
if (!calendarPopup) {
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml");
if (component.status === Component.Ready) {
calendarPopup = component.createObject(calendarButton.parent, {
"targetX": calendarButton.x + calendarButton.width + 10,
"shell": calendarButton.shell
});
} else if (component.status === Component.Error) {
console.log("Error loading calendar:", component.errorString());
return;
}
}
if (calendarPopup) {
calendarVisible = !calendarVisible;
calendarPopup.setClickMode(calendarVisible);
}
}
function hideCalendar() {
if (calendarPopup) {
calendarVisible = false;
calendarPopup.setClickMode(false);
}
}
}

View File

@@ -0,0 +1,309 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
// Night light widget with pure Qt overlay (no external dependencies)
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: nightLightMouseArea.containsMouse
property bool isActive: Data.Settings.nightLightEnabled
property real warmth: Data.Settings.nightLightWarmth // 0=no filter, 1=very warm (0-1 scale)
property real strength: isActive ? warmth : 0
property bool autoSchedulerActive: false // Flag to prevent manual override during auto changes
signal entered
signal exited
// Night light overlay window
property var overlayWindow: null
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered();
} else {
exited();
}
}
// Background with warm tint when active
Rectangle {
anchors.fill: parent
radius: parent.radius
color: isActive ? Qt.rgba(1.0, 0.6, 0.2, 0.15) : "transparent"
Behavior on color {
ColorAnimation {
duration: 300
}
}
}
MouseArea {
id: nightLightMouseArea
anchors.fill: parent
hoverEnabled: true
// Right-click to cycle through warmth levels
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function (mouse) {
if (mouse.button === Qt.RightButton) {
cycleWarmth();
} else {
toggleNightLight();
}
}
}
// Night light icon with dynamic color
Text {
anchors.centerIn: parent
text: isActive ? "light_mode" : "dark_mode"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: isActive ? Qt.rgba(1.0, 0.8 - strength * 0.3, 0.4 - strength * 0.2, 1.0) : // Warm orange when active
(containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor)
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
// Warmth indicator dots
Row {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
visible: isActive && containsMouse
Repeater {
model: 3
delegate: Rectangle {
width: 4
height: 4
radius: 2
color: index < Math.ceil(warmth * 3) ? Qt.rgba(1.0, 0.7 - index * 0.2, 0.3, 0.8) : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
}
}
// Watch for settings changes
Connections {
target: Data.Settings
function onNightLightEnabledChanged() {
if (Data.Settings.nightLightEnabled) {
createOverlay();
} else {
removeOverlay();
}
// Set manual override flag if this wasn't an automatic change
if (!autoSchedulerActive) {
Data.Settings.nightLightManualOverride = true;
Data.Settings.nightLightManuallyEnabled = Data.Settings.nightLightEnabled;
console.log("Manual night light change detected - override enabled, manually set to:", Data.Settings.nightLightEnabled);
}
}
function onNightLightWarmthChanged() {
updateOverlay();
}
}
// Functions to control night light
function toggleNightLight() {
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled;
}
function cycleWarmth() {
// Cycle through warmth levels: 0.2 -> 0.4 -> 0.6 -> 1.0 -> 0.2
var newWarmth = warmth >= 1.0 ? 0.2 : (warmth >= 0.6 ? 1.0 : warmth + 0.2);
Data.Settings.nightLightWarmth = newWarmth;
}
function createOverlay() {
if (overlayWindow)
return;
var qmlString = `
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
id: nightLightOverlay
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-nightlight"
exclusiveZone: 0
// Click-through overlay
mask: Region {}
Rectangle {
id: overlayRect
anchors.fill: parent
color: "transparent" // Initial color, will be set by parent
// Smooth transitions when warmth changes
Behavior on color {
ColorAnimation { duration: 300 }
}
}
// Function to update overlay color
function updateColor(newWarmth) {
overlayRect.color = Qt.rgba(1.0, 0.8 - newWarmth * 0.4, 0.3 - newWarmth * 0.25, 0.1 + newWarmth * 0.2)
}
}
`
try {
overlayWindow = Qt.createQmlObject(qmlString, root);
// Set initial color
updateOverlay();
} catch (e) {
console.error("Failed to create night light overlay:", e);
}
}
function updateOverlay() {
if (overlayWindow && overlayWindow.updateColor) {
overlayWindow.updateColor(warmth);
}
}
function removeOverlay() {
if (overlayWindow) {
overlayWindow.destroy();
overlayWindow = null;
}
}
// Preset warmth levels for easy access
function setLow() {
Data.Settings.nightLightWarmth = 0.2;
} // Light warmth
function setMedium() {
Data.Settings.nightLightWarmth = 0.4;
} // Medium warmth
function setHigh() {
Data.Settings.nightLightWarmth = 0.6;
} // High warmth
function setMax() {
Data.Settings.nightLightWarmth = 1.0;
} // Maximum warmth
// Auto-enable based on time (basic sunset/sunrise simulation)
Timer {
interval: 60000 // Check every minute
running: true
repeat: true
onTriggered: checkAutoEnable()
}
function checkAutoEnable() {
if (!Data.Settings.nightLightAuto)
return;
var now = new Date();
var hour = now.getHours();
var minute = now.getMinutes();
var startHour = Data.Settings.nightLightStartHour || 20;
var endHour = Data.Settings.nightLightEndHour || 6;
// Handle overnight schedules (e.g., 20:00 to 6:00)
var shouldBeActive = false;
if (startHour > endHour) {
// Overnight: active from startHour onwards OR before endHour
shouldBeActive = (hour >= startHour || hour < endHour);
} else if (startHour < endHour) {
// Same day: active between startHour and endHour
shouldBeActive = (hour >= startHour && hour < endHour);
} else {
// startHour === endHour: never auto-enable
shouldBeActive = false;
}
// Debug logging
console.log(`Night Light Auto Check: ${hour}:${minute.toString().padStart(2, '0')} - Should be active: ${shouldBeActive}, Currently active: ${Data.Settings.nightLightEnabled}, Manual override: ${Data.Settings.nightLightManualOverride}`);
// Smart override logic - only block conflicting actions
if (Data.Settings.nightLightManualOverride) {
// If user manually enabled, allow auto-disable but block auto-enable
if (Data.Settings.nightLightManuallyEnabled && !shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light (respecting schedule after manual enable)");
autoSchedulerActive = true;
Data.Settings.nightLightEnabled = false;
Data.Settings.nightLightManualOverride = false; // Reset after respecting schedule
autoSchedulerActive = false;
return;
} else
// If user manually disabled, block auto-enable until next cycle
if (!Data.Settings.nightLightManuallyEnabled && shouldBeActive && !Data.Settings.nightLightEnabled) {
// Check if this is the start of a new schedule cycle
var isNewCycle = (hour === startHour && minute === 0);
if (isNewCycle) {
console.log("New schedule cycle starting - resetting manual override");
Data.Settings.nightLightManualOverride = false;
} else {
console.log("Manual disable override active - skipping auto-enable");
return;
}
} else
// Other cases - reset override and continue
{
Data.Settings.nightLightManualOverride = false;
}
}
// Auto-enable when schedule starts
if (shouldBeActive && !Data.Settings.nightLightEnabled) {
console.log("Auto-enabling night light");
autoSchedulerActive = true;
Data.Settings.nightLightEnabled = true;
autoSchedulerActive = false;
} else
// Auto-disable when schedule ends
if (!shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light");
autoSchedulerActive = true;
Data.Settings.nightLightEnabled = false;
autoSchedulerActive = false;
}
}
// Cleanup on destruction
Component.onDestruction: {
removeOverlay();
}
// Initialize overlay state based on settings
Component.onCompleted: {
if (Data.Settings.nightLightEnabled) {
createOverlay();
}
}
}

View File

@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
// Screen recording toggle button
Rectangle {
id: root
required property var shell
required property bool isRecording
radius: 20
signal recordingRequested
signal stopRecordingRequested
signal mouseChanged(bool containsMouse)
// Dynamic color: accent when recording/hovered, gray otherwise
color: isRecording ? Data.ThemeManager.accentColor : (mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
// Button content with icon and text
RowLayout {
anchors.centerIn: parent
spacing: 10
// Recording state icon
Text {
text: isRecording ? "stop_circle" : "radio_button_unchecked"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
// Recording state label
Label {
text: isRecording ? "Stop Recording" : "Start Recording"
font.family: "monospace"
font.pixelSize: 13
font.weight: Font.Medium
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
}
// Click handling and hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: {
if (isRecording) {
root.stopRecordingRequested();
} else {
root.recordingRequested();
}
}
}
}

View File

@@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Simple theme toggle button with hover feedback
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: themeMouseArea.containsMouse
property bool menuJustOpened: false
signal entered
signal exited
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered();
} else if (!menuJustOpened) {
exited();
}
}
MouseArea {
id: themeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.ThemeManager.toggleTheme();
}
}
// Theme toggle icon with color feedback
Label {
anchors.centerIn: parent
text: "contrast"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}

View File

@@ -0,0 +1,239 @@
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data/" as Data
// User profile card
Rectangle {
id: root
required property var shell
property url avatarSource: Data.Settings.avatarSource
property string userName: "" // will be set by process output
property string userInfo: "" // will hold uptime string
property bool isActive: false
property bool isHovered: false // track hover state
radius: 20
width: 220
height: 80
// Dynamic color based on hover and active states
color: {
if (isActive) {
return isHovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3);
} else {
return isHovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2) : Qt.lighter(Data.ThemeManager.bgColor, 1.15);
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
Row {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.verticalCenter: parent.verticalCenter
// Avatar
Rectangle {
id: avatarCircle
width: 52
height: 52
radius: 20
clip: true
border.color: Data.ThemeManager.accentColor
border.width: 3
color: "transparent"
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hidden for masking
asynchronous: true
sourceSize.width: 48 // Memory optimization
sourceSize.height: 48
}
// Apply circular mask to avatar
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 18 // Proportional to parent radius
visible: false
}
}
}
// User information text
Column {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
Text {
width: parent.width
text: root.userName === "" ? "Loading..." : root.userName
font.family: "monospace"
font.pixelSize: 16
font.bold: true
color: isHovered || root.isActive ? "#ffffff" : Data.ThemeManager.accentColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
font.family: "monospace"
font.pixelSize: 11
font.bold: true
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
elide: Text.ElideRight
maximumLineCount: 1
}
}
// Animated GIF with rounded corners
Rectangle {
id: gifContainer
width: 80
height: 80
radius: 12
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
AnimatedImage {
id: animatedImage
source: "root:/Assets/UserProfile.gif"
anchors.fill: parent
fillMode: Image.PreserveAspectFit
playing: true
cache: false
speed: 1.0
asynchronous: true
}
// Apply rounded corner mask to GIF
layer.enabled: true
layer.effect: OpacityMask {
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: gifContainer.width
height: gifContainer.height
radius: gifContainer.radius
visible: false
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
}
// Get current username
Process {
id: usernameProcess
running: true // Always run to get username
command: ["sh", "-c", "whoami"]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
const line = data.trim();
if (line.length > 0) {
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
}
}
}
}
// Get system uptime with parsing for readable format
Process {
id: uptimeProcess
running: false
command: ["sh", "-c", "uptime"] // Use basic uptime command
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
const line = data.trim();
if (line.length > 0) {
// Parse uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
if (match && match[1]) {
root.userInfo = "Up: " + match[1].trim();
} else {
// Fallback parsing for different uptime formats
const upIndex = line.indexOf("up ");
if (upIndex !== -1) {
const afterUp = line.substring(upIndex + 3);
const commaIndex = afterUp.indexOf(",");
if (commaIndex !== -1) {
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
} else {
root.userInfo = "Up: " + afterUp.trim();
}
} else {
root.userInfo = "Uptime unknown";
}
}
} else {
root.userInfo = "Uptime unknown";
}
}
}
stderr: SplitParser {
splitMarker: "\n"
onRead: data => {
console.log("Uptime error:", data);
root.userInfo = "Uptime error";
}
}
}
// Update uptime every 5 minutes
Timer {
id: uptimeTimer
interval: 300000 // Update every 5 minutes
running: true // Always run the uptime timer
repeat: true
onTriggered: {
uptimeProcess.running = false;
uptimeProcess.running = true;
}
}
Component.onCompleted: {
uptimeProcess.running = true; // Start uptime process on component load
}
Component.onDestruction: {
if (usernameProcess.running) {
usernameProcess.running = false;
}
if (uptimeProcess.running) {
uptimeProcess.running = false;
}
if (uptimeTimer.running) {
uptimeTimer.running = false;
}
}
}

View File

@@ -0,0 +1,383 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Weather display widget
Rectangle {
id: root
required property var shell
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
property bool menuJustOpened: false
signal entered
signal exited
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered();
} else if (!menuJustOpened && !forecastPopup.visible) {
exited();
}
}
// Maps WMO weather condition codes and text descriptions to Material Design icons
function getWeatherIcon(condition) {
if (!condition)
return "light_mode";
const c = condition.toString();
// WMO weather interpretation codes to Material Design icons
const iconMap = {
"0": "light_mode" // Clear sky
,
"1": "light_mode" // Mainly clear
,
"2": "cloud" // Partly cloudy
,
"3": "cloud" // Overcast
,
"45": "foggy" // Fog
,
"48": "foggy" // Depositing rime fog
,
"51": "water_drop" // Light drizzle
,
"53": "water_drop" // Moderate drizzle
,
"55": "water_drop" // Dense drizzle
,
"61": "water_drop" // Slight rain
,
"63": "water_drop" // Moderate rain
,
"65": "water_drop" // Heavy rain
,
"71": "ac_unit" // Slight snow
,
"73": "ac_unit" // Moderate snow
,
"75": "ac_unit" // Heavy snow
,
"80": "water_drop" // Slight rain showers
,
"81": "water_drop" // Moderate rain showers
,
"82": "water_drop" // Violent rain showers
,
"95": "thunderstorm" // Thunderstorm
,
"96": "thunderstorm" // Thunderstorm with light hail
,
"99": "thunderstorm" // Thunderstorm with heavy hail
};
if (iconMap[c])
return iconMap[c];
// Fallback text matching for non-WMO weather APIs
const textMap = {
"clear sky": "light_mode",
"mainly clear": "light_mode",
"partly cloudy": "cloud",
"overcast": "cloud",
"fog": "foggy",
"drizzle": "water_drop",
"rain": "water_drop",
"snow": "ac_unit",
"thunderstorm": "thunderstorm"
};
const lower = condition.toLowerCase();
for (let key in textMap) {
if (lower.includes(key))
return textMap[key];
}
return "help"; // Unknown condition fallback
}
// Hover trigger for forecast popup
MouseArea {
id: weatherMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
menuJustOpened = true;
forecastPopup.open();
Qt.callLater(() => menuJustOpened = false);
}
onExited: {
if (!forecastPopup.containsMouse && !menuJustOpened) {
forecastPopup.close();
}
}
}
// Compact weather display (icon and temperature)
RowLayout {
id: weatherLayout
anchors.centerIn: parent
spacing: 8
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
// Weather condition icon
Label {
text: {
if (shell.weatherLoading)
return "refresh";
if (!shell.weatherData)
return "help";
return root.getWeatherIcon(shell.weatherData.currentCondition);
}
font.pixelSize: 28
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
Layout.alignment: Qt.AlignHCenter
}
// Current temperature
Label {
text: {
if (shell.weatherLoading)
return "Loading...";
if (!shell.weatherData)
return "No weather data";
return shell.weatherData.currentTemp;
}
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 20
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
}
}
// Forecast popup
Popup {
id: forecastPopup
y: parent.height + 28
x: Math.min(0, parent.width - width)
width: 300
height: 226
padding: 12
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
property bool containsMouse: forecastMouseArea.containsMouse
onVisibleChanged: {
if (visible) {
entered();
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
exited();
}
}
// Hover area for popup persistence
MouseArea {
id: forecastMouseArea
anchors.fill: parent
hoverEnabled: true
onExited: {
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
forecastPopup.close();
}
}
}
ColumnLayout {
id: forecastColumn
anchors.fill: parent
anchors.margins: 10
spacing: 8
// Current weather detailed view
RowLayout {
Layout.fillWidth: true
spacing: 12
// Large weather icon
Label {
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
font.pixelSize: 48
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
// Weather condition description
Label {
text: shell.weatherData ? shell.weatherData.currentCondition : ""
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
// Weather metrics: temperature, wind, direction
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
// Temperature metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "thermostat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: shell.weatherData ? shell.weatherData.currentTemp : ""
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind speed metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "air"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details)
return "";
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"));
return windInfo ? windInfo.split(": ")[1] : "";
}
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind direction metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "explore"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details)
return "";
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"));
return dirInfo ? dirInfo.split(": ")[1] : "";
}
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 12
}
}
Item {
Layout.fillWidth: true
}
}
}
}
// Section separator
Rectangle {
height: 1
Layout.fillWidth: true
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
Label {
text: "3-Day Forecast"
color: Data.ThemeManager.accentColor
font.family: "monospace"
font.pixelSize: 12
font.bold: true
}
// Three-column forecast cards
Row {
spacing: 8
Layout.fillWidth: true
Repeater {
model: shell.weatherData ? shell.weatherData.forecast : []
delegate: Column {
width: (parent.width - 16) / 3
spacing: 2
// Day name
Label {
text: modelData.dayName
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 10
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
// Weather icon
Label {
text: root.getWeatherIcon(modelData.condition)
font.pixelSize: 16
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
// Temperature range
Label {
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
color: Data.ThemeManager.fgColor
font.family: "monospace"
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}