1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/quickshell/Modules/DankDash/WeatherTab.qml
mbpowers 17639e8729 feat: add sun and moon view to WeatherTab (#787)
* feat: add sun and moon view to WeatherTab

* feat: hourly forecast and scrollable date

* fix: put listviews in loaders to prevent ui blocking

* dankdash/weather: wrap all tab content in loaders, weather updates
- remove a bunch of transitions that make things feel glitchy
- use animation durations from Theme
- configurable detailed/compact hourly view

* weather: fix scroll and some display issues

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2025-11-30 18:47:27 -05:00

1111 lines
47 KiB
QML

import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankBar.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
property bool syncing: false
function syncFrom(type) {
if (!dailyLoader.item || !hourlyLoader.item)
return;
const hourlyList = hourlyLoader.item;
const dailyList = dailyLoader.item;
syncing = true;
try {
if (type === "hour") {
const date = new Date();
date.setHours(hourlyList.currentIndex);
dateStepper.currentDate = date;
dailyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.forecast?.length ?? 1) - 1, WeatherService.calendarDayDifference((new Date()), date)));
} else if (type === "day") {
const date = new Date(dateStepper.currentDate);
date.setMonth((new Date()).getMonth());
date.setDate((new Date()).getDate() + dailyList.currentIndex);
dateStepper.currentDate = date;
const hourIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date).getHours()));
hourlyList.currentIndex = hourIndex;
} else if (type === "date") {
const date = dateStepper.currentDate;
dailyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.forecast?.length ?? 1) - 1, WeatherService.calendarDayDifference((new Date()), date)));
hourlyList.currentIndex = Math.max(0, Math.min((WeatherService.weather.hourlyForecast?.length ?? 1) - 1, WeatherService.calendarHourDifference((new Date()), date) + (new Date()).getHours()));
}
} catch (e) {
console.warn("Weather Date Sync Error:", e);
}
syncing = false;
}
property bool available: WeatherService.weather.available
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: !root.available
DankIcon {
name: "cloud_off"
size: Theme.iconSize * 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
width: refreshButtonTwo.width + refreshText.width
height: refreshButtonTwo.height
spacing: Theme.spacingS
StyledText {
id: refreshText
text: I18n.tr("No Weather Data Available")
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
id: refreshButtonTwo
name: "refresh"
size: Theme.iconSize - 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
anchors.top: parent.top
anchors.verticalCenter: parent.verticalCenter
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
id: refreshButtonMouseAreaTwo
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
enabled: parent.enabled
Timer {
id: hoverDelayTwo
interval: 1000
repeat: false
onTriggered: {
const p = refreshButtonMouseAreaTwo.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS);
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
}
}
onEntered: {
hoverDelayTwo.restart();
}
onExited: {
hoverDelayTwo.stop();
refreshButtonTooltipTwo.hide();
}
onClicked: {
refreshButtonTwo.isRefreshing = true;
WeatherService.forceRefresh();
refreshTimerTwo.restart();
}
}
DankTooltip {
id: refreshButtonTooltipTwo
}
Timer {
id: refreshTimerTwo
interval: 2000
onTriggered: refreshButtonTwo.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButtonTwo.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
}
}
Column {
anchors.fill: parent
visible: root.available
spacing: Theme.spacingXS
Item {
id: weatherContainer
width: parent.width
height: weatherColumn.height
Column {
id: weatherColumn
height: weatherInfo.height + dateStepper.height
width: Math.max(weatherInfo.width, dateStepper.width)
Item {
id: weatherInfo
anchors.horizontalCenter: parent.horizontalCenter
// anchors.verticalCenter: parent.verticalCenter
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
height: 70
DankIcon {
id: weatherIcon
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize * 1.5
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
Column {
id: tempColumn
spacing: Theme.spacingXS
anchors.left: weatherIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Item {
width: tempText.width + unitText.width + Theme.spacingXS
height: tempText.height
StyledText {
id: tempText
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
font.pixelSize: Theme.fontSizeLarge + 4
color: Theme.surfaceText
font.weight: Font.Light
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: unitText
text: SettingsData.useFahrenheit ? "F" : "C"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.left: tempText.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available) {
SettingsData.set("useFahrenheit", !SettingsData.useFahrenheit);
}
}
enabled: WeatherService.weather.available
}
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
Column {
id: sunriseColumn
spacing: Theme.spacingXS
anchors.left: tempColumn.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
Item {
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
height: sunriseIcon.height
DankIcon {
id: sunriseIcon
name: "wb_twilight"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunriseText
text: WeatherService.weather.sunrise || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunriseIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
height: sunsetIcon.height
DankIcon {
id: sunsetIcon
name: "bedtime"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunsetText
text: WeatherService.weather.sunset || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunsetIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
Item {
id: dateStepper
anchors.horizontalCenter: parent.horizontalCenter
height: 60
width: dateStepperInner.width
property var currentDate: new Date()
readonly property var changeDate: (magnitudeIndex, sign) => {
switch (magnitudeIndex) {
case 0:
break;
case 1:
var newDate = new Date(dateStepper.currentDate);
newDate.setMonth(dateStepper.currentDate.getMonth() + sign * 1);
dateStepper.currentDate = newDate;
break;
case 2:
dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 24 * 3600 * 1000);
break;
case 3:
dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 3600 * 1000);
break;
case 4:
dateStepper.currentDate = new Date(dateStepper.currentDate.getTime() + sign * 5 * 60 * 1000);
break;
}
}
readonly property var splitDate: Qt.formatDateTime(dateStepper.currentDate, "yyyy.MM.dd.hh.mm.AP").split('.')
Item {
id: dateStepperInner
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
readonly property var space: Theme.spacingXS
width: yearStepper.width + monthStepper.width + dayStepper.width + hourStepper.width + minuteStepper.width + suffix.width + 10.5 * space + 2 * dateStepperInnerPadding.width
Item {
id: dateStepperInnerPadding
width: dateResetButton.width
}
DankNumberStepper {
id: yearStepper
anchors.left: dateStepperInnerPadding.right
anchors.leftMargin: parent.space
width: implicitWidth
text: dateStepper.splitDate[0]
// onIncrement: () => dateStepper.changeDate(0, +1)
// onDecrement: () => dateStepper.changeDate(0, -1)
}
DankNumberStepper {
id: monthStepper
width: implicitWidth
anchors.left: yearStepper.right
anchors.leftMargin: parent.space
text: dateStepper.splitDate[1]
onIncrement: () => dateStepper.changeDate(1, +1)
onDecrement: () => dateStepper.changeDate(1, -1)
}
DankNumberStepper {
id: dayStepper
width: implicitWidth
anchors.left: monthStepper.right
anchors.leftMargin: parent.space
text: dateStepper.splitDate[2]
onIncrement: () => dateStepper.changeDate(2, +1)
onDecrement: () => dateStepper.changeDate(2, -1)
}
DankNumberStepper {
id: hourStepper
width: implicitWidth
anchors.left: dayStepper.right
anchors.leftMargin: 1.5 * parent.space
text: dateStepper.splitDate[3]
onIncrement: () => dateStepper.changeDate(3, +1)
onDecrement: () => dateStepper.changeDate(3, -1)
}
DankNumberStepper {
id: minuteStepper
width: implicitWidth
anchors.left: hourStepper.right
anchors.leftMargin: parent.space
text: dateStepper.splitDate[4]
onIncrement: () => dateStepper.changeDate(4, +1)
onDecrement: () => dateStepper.changeDate(4, -1)
}
Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: yearStepper.right
anchors.right: monthStepper.left
StyledText {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: "-"
}
}
Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: monthStepper.right
anchors.right: dayStepper.left
StyledText {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: "-"
}
}
Item {
anchors.verticalCenter: parent.verticalCenter
anchors.left: hourStepper.right
anchors.right: minuteStepper.left
StyledText {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
text: ":"
}
}
Rectangle {
id: suffix
anchors.verticalCenter: parent.verticalCenter
anchors.left: minuteStepper.right
anchors.leftMargin: 2 * parent.space
StyledText {
isMonospace: true
anchors.horizontalCenter: parent.horizontalCenter
text: dateStepper.splitDate[5]
font.pixelSize: Theme.fontSizeSmall
x: -Theme.fontSizeSmall / 2
y: -Theme.fontSizeSmall / 2
}
}
DankActionButton {
id: dateResetButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
enabled: Math.abs(dateStepper.currentDate - new Date()) > 1000
iconColor: enabled ? Theme.blendAlpha(Theme.surfaceText, 0.5) : "transparent"
iconSize: 12
buttonSize: 20
iconName: "replay"
onClicked: {
dateStepper.currentDate = new Date();
}
}
}
onCurrentDateChanged: if (!syncing)
root.syncFrom("date")
}
}
Rectangle {
id: skyBox
height: weatherColumn.height
anchors.left: weatherColumn.right
anchors.right: parent.right
anchors.leftMargin: Theme.spacingM
property var backgroundOpacity: 0.3
property var sunTime: WeatherService.getCurrentSunTime(dateStepper.currentDate)
property var periodIndex: sunTime?.periodIndex
property var periodPercent: sunTime?.periodPercent
property var blackColor: Theme.blend(Theme.surface, Qt.rgba(0, 0, 0, 255), 0.2)
property var redColor: Theme.secondary
property var blueColor: Theme.primary
function blackBlue(r) {
return Theme.blend(blackColor, blueColor, r);
}
property var topColor: {
const colorMap = [blackColor // "night"
, Theme.withAlpha(blackBlue(0.0), 0.8) // "astronomicalTwilight"
, Theme.withAlpha(blackBlue(0.2), 0.7) // "nauticalTwilight"
, Theme.withAlpha(blackBlue(0.5), 0.6) // "civilTwilight"
, Theme.withAlpha(blackBlue(0.7), 0.6) // "sunrise"
, Theme.withAlpha(blackBlue(0.9), 0.6) // "goldenHourMorning"
, Theme.withAlpha(blackBlue(1.0), 0.6) // "daytime"
, Theme.withAlpha(blackBlue(0.9), 0.6) // "afternoon"
, Theme.withAlpha(blackBlue(0.7), 0.6) // "goldenHourEvening"
, Theme.withAlpha(blackBlue(0.5), 0.6) // "sunset"
, Theme.withAlpha(blackBlue(0.2), 0.7) // "civilTwilight"
, Theme.withAlpha(blackBlue(0.0), 0.8) // "nauticalTwilightEvening"
, blackColor // "astronomicalTwilightEvening"
, blackColor // "night"
,];
const index = periodIndex ?? 0;
return Theme.blend(colorMap[index], colorMap[index + 1], periodPercent ?? 0);
}
property var sunColor: {
const colorMap = [Theme.withAlpha(redColor, 0.05) // "night"
, Theme.withAlpha(redColor, 0.1) // "astronomicalTwilight"
, Theme.withAlpha(redColor, 0.3) // "nauticalTwilight"
, Theme.withAlpha(redColor, 0.4) // "civilTwilight"
, Theme.withAlpha(redColor, 0.5) // "sunrise"
, Theme.withAlpha(blueColor, 0.2) // "goldenHourMorning"
, Theme.withAlpha(blueColor, 0.0) // "daytime"
, Theme.withAlpha(blueColor, 0.2) // "afternoon"
, Theme.withAlpha(redColor, 0.5) // "goldenHourEvening"
, Theme.withAlpha(redColor, 0.4) // "sunset"
, Theme.withAlpha(redColor, 0.3) // "civilTwilight"
, Theme.withAlpha(redColor, 0.1) // "nauticalTwilightEvening"
, Theme.withAlpha(redColor, 0.05) // "astronomicalTwilightEvening"
, Theme.withAlpha(redColor, 0.0) // "night"
,];
const index = periodIndex ?? 0;
return Theme.blend(colorMap[index], colorMap[index + 1], periodPercent ?? 0);
}
color: "transparent"
Rectangle {
anchors.fill: parent
opacity: skyBox.backgroundOpacity
gradient: Gradient {
GradientStop {
position: 0.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
GradientStop {
position: 0.05
color: skyBox.topColor
}
GradientStop {
position: 0.3
color: skyBox.topColor
}
GradientStop {
position: 0.5
color: skyBox.topColor
}
GradientStop {
position: 0.501
color: skyBox.blackColor
}
GradientStop {
position: 0.9
color: skyBox.blackColor
}
GradientStop {
position: 1.0
color: Theme.withAlpha(skyBox.blackColor, 0.0)
}
}
}
property var currentDate: dateStepper.currentDate
property var hMargin: 0
property var vMargin: Theme.spacingM
property var effectiveHeight: skyBox.height - 2 * vMargin
property var effectiveWidth: skyBox.width - 2 * hMargin
StyledText {
text: parent.sunTime?.period ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.withAlpha(Theme.surfaceText, 0.7)
x: 0
y: 0
}
Shape {
id: skyShape
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
height: parent.height / 2
opacity: skyBox.backgroundOpacity
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: skyBox.hMargin + sun.x + sun.width / 2
centerY: skyBox.vMargin + sun.y + 30
centerRadius: {
const a = Math.abs((skyBox.sunTime?.dayPercent ?? 0) - 0.5);
const out = 200 * (0.5 - a * a);
return out;
}
focalX: skyBox.hMargin + sun.x + sun.width / 2
focalY: skyBox.vMargin + sun.y
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.3
color: Theme.blendAlpha(skyBox.sunColor, 0.5)
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
ShapePath {
strokeColor: "transparent"
fillGradient: RadialGradient {
centerX: sun.x
centerY: sun.y
centerRadius: 500
focalX: centerX
focalY: centerY + 0.99 * (centerRadius - focalRadius)
focalRadius: 10
GradientStop {
position: 0
color: skyBox.sunColor
}
GradientStop {
position: 0.45
color: skyBox.sunColor
}
GradientStop {
position: 0.55
color: "transparent"
}
GradientStop {
position: 1
color: "transparent"
}
}
PathLine {
x: 0
y: 0
}
PathLine {
x: skyShape.width
y: 0
}
PathLine {
x: skyShape.width
y: skyShape.height
}
PathLine {
x: 0
y: skyShape.height
}
}
}
Canvas {
id: ecliptic
anchors.fill: parent
property var points: WeatherService.getEcliptic(dateStepper.currentDate)
function getX(index) {
return points[index].h * skyBox.effectiveWidth + skyBox.hMargin;
}
function getY(index) {
return points[index].v * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 + skyBox.vMargin;
}
onPointsChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
if (!points || points.length === 0)
return;
ctx.beginPath();
ctx.moveTo(getX(0), getY(0));
for (var i = 1; i < points.length; i++) {
ctx.lineTo(getX(i), getY(i));
}
ctx.strokeStyle = Theme.withAlpha(Theme.outline, 0.2);
ctx.stroke();
}
}
property real latitude: WeatherService.getLocation()?.latitude ?? 0
property real sunDeclination: WeatherService.getSunDeclination(dateStepper.currentDate)
readonly property bool solarNoonIsSouth: latitude > sunDeclination
StyledText {
id: middle
text: skyBox.solarNoonIsSouth ? "S" : "N"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 2 - middle.width / 2
y: skyBox.height / 2 - middle.height / 2
}
StyledText {
id: left
text: skyBox.solarNoonIsSouth ? "E" : "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: skyBox.width / 4 - left.width / 2
y: skyBox.height / 2 - left.height / 2
}
StyledText {
id: right
text: skyBox.solarNoonIsSouth ? "W" : "E"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
x: 3 * skyBox.width / 4 - right.width / 2
y: skyBox.height / 2 - right.height / 2
}
Rectangle {
// Rightmost Line
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: right.right
anchors.right: skyBox.right
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
// Middle Right Line
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: middle.right
anchors.right: right.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
// Middle Left Line
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: left.right
anchors.right: middle.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
Rectangle {
// Leftmost Line
height: 1
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.left: skyBox.left
anchors.right: left.left
anchors.verticalCenter: middle.verticalCenter
color: Theme.outline
}
StyledText {
id: moonPhase
text: WeatherService.getMoonPhase(skyBox.currentDate) || ""
font.pixelSize: Theme.fontSizeXLarge * 1
color: Theme.withAlpha(Theme.surfaceText, 0.7)
rotation: (WeatherService.getMoonAngle(skyBox.currentDate) || 0) / Math.PI * 180
visible: !!pos
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, false)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
StyledText {
id: sun
text: ""
font.pixelSize: Theme.fontSizeXLarge * 1
color: Theme.primary
visible: !!pos
property var pos: WeatherService.getSkyArcPosition(skyBox.currentDate, true)
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
}
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
anchors.right: parent.right
anchors.top: parent.top
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
id: refreshButtonMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
enabled: parent.enabled
Timer {
id: hoverDelay
interval: 1000
repeat: false
onTriggered: {
const p = refreshButtonMouseArea.mapToItem(null, parent.width / 2, parent.height + Theme.spacingXS);
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), p.x, p.y, null);
}
}
onEntered: {
hoverDelay.restart();
}
onExited: {
hoverDelay.stop();
refreshButtonTooltip.hide();
}
onClicked: {
refreshButton.isRefreshing = true;
WeatherService.forceRefresh();
refreshTimer.restart();
}
}
DankTooltip {
id: refreshButtonTooltip
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
}
Row {
width: parent.width
height: 32
spacing: Theme.spacingS
StyledText {
id: hourlyHeader
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Hourly Forecast")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - denseButton.width - hourlyHeader.width - 2 * parent.spacing
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
DankActionButton {
id: denseButton
anchors.verticalCenter: parent.verticalCenter
visible: hourlyLoader.item !== null
iconName: SessionData.weatherHourlyDetailed ? "tile_large" : "tile_medium"
onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed)
}
}
Item {
width: parent.width
height: 100 + Theme.spacingXS
Loader {
id: hourlyLoader
anchors.fill: parent
sourceComponent: hourlyComponent
active: root.visible && root.available
asynchronous: true
opacity: 0
onLoaded: {
root.syncing = true;
item.currentIndex = item.initialIndex;
item.positionViewAtIndex(item.initialIndex, ListView.SnapPosition);
root.syncing = false;
opacity = 1;
}
}
Component {
id: hourlyComponent
ListView {
id: hourlyList
width: parent.width
height: cardHeight + Theme.spacingXS
orientation: ListView.Horizontal
spacing: Theme.spacingS
clip: true
snapMode: ListView.SnapToItem
highlightRangeMode: ListView.StrictlyEnforceRange
highlightMoveDuration: 0
interactive: true
contentHeight: cardHeight
contentWidth: cardWidth
property var cardHeight: 100
property var cardWidth: ((hourlyList.width + hourlyList.spacing) / hourlyList.visibleCount) - hourlyList.spacing
property int initialIndex: (new Date()).getHours()
property bool dense: !SessionData.weatherHourlyDetailed
property int visibleCount: 8
model: WeatherService.weather.hourlyForecast?.length ?? 0
delegate: WeatherForecastCard {
width: hourlyList.cardWidth
height: hourlyList.cardHeight
dense: hourlyList.dense
daily: false
date: {
const d = new Date();
d.setHours(index);
return d;
}
forecastData: WeatherService.weather.hourlyForecast?.[index]
}
onCurrentIndexChanged: if (!syncing)
root.syncFrom("hour")
states: [
State {
name: "denseState"
when: hourlyList.dense
PropertyChanges {
target: hourlyList
visibleCount: 10
}
},
State {
name: "normalState"
when: !hourlyList.dense
PropertyChanges {
target: hourlyList
visibleCount: 5
}
}
]
transitions: [
Transition {
NumberAnimation {
properties: "visibleCount"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.modifiers & Qt.ShiftModifier) {
if (wheel.angleDelta.y % 120 == 0 && wheel.angleDelta.x == 0) {
const newIndex = hourlyList.currentIndex - Math.sign(wheel.angleDelta.y);
if (newIndex < hourlyList.model && newIndex >= 0) {
hourlyList.currentIndex = newIndex;
wheel.accepted = true;
return;
}
}
}
wheel.accepted = false;
}
}
}
}
}
Row {
width: parent.width
height: 32
spacing: Theme.spacingS
StyledText {
id: dailyHeader
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Daily Forecast")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - denseButton.width - dailyHeader.width - 2 * parent.spacing
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
}
Item {
width: parent.width
height: 100 + Theme.spacingXS
Loader {
id: dailyLoader
anchors.fill: parent
sourceComponent: dailyComponent
active: root.visible && root.available
asynchronous: true
opacity: 0
onLoaded: {
root.syncing = true;
item.currentIndex = item.initialIndex;
item.positionViewAtIndex(item.initialIndex, ListView.SnapPosition);
root.syncing = false;
opacity = 1;
}
}
Component {
id: dailyComponent
ListView {
id: dailyList
width: parent.width
height: cardHeight + Theme.spacingXS
orientation: ListView.Horizontal
spacing: Theme.spacingS
clip: true
snapMode: ListView.SnapToItem
highlightRangeMode: ListView.StrictlyEnforceRange
highlightMoveDuration: 0
interactive: true
contentHeight: cardHeight
contentWidth: cardWidth
property var cardHeight: 100
property var cardWidth: ((dailyList.width + dailyList.spacing) / dailyList.visibleCount) - dailyList.spacing
property int initialIndex: 0
property bool dense: false
property int visibleCount: 7
model: WeatherService.weather.forecast?.length ?? 0
delegate: WeatherForecastCard {
width: dailyList.cardWidth
height: dailyList.cardHeight
dense: true
daily: true
date: {
const date = new Date();
date.setDate(date.getDate() + index);
return date;
}
forecastData: WeatherService.weather.forecast?.[index]
}
onCurrentIndexChanged: if (!syncing)
root.syncFrom("day")
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.modifiers & Qt.ShiftModifier) {
if (wheel.angleDelta.y % 120 == 0 && wheel.angleDelta.x == 0) {
const newIndex = dailyList.currentIndex - Math.sign(wheel.angleDelta.y);
if (newIndex < dailyList.model && newIndex >= 0) {
dailyList.currentIndex = newIndex;
wheel.accepted = true;
return;
}
}
}
wheel.accepted = false;
}
}
}
}
}
}
}