summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml1124
1 files changed, 1124 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml
new file mode 100644
index 0000000..21ea656
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Modules/DankDash/WeatherTab.qml
@@ -0,0 +1,1124 @@
+import QtQuick
+import QtQuick.Effects
+import QtQuick.Shapes
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ implicitWidth: SettingsData.showWeekNumber ? 736 : 700
+ implicitHeight: 410
+ property bool syncing: false
+ property bool showHourly: false
+ property bool available: WeatherService.weather.available
+
+ 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;
+ }
+
+ readonly property string sunriseTimeText: {
+ if (!WeatherService.weather.rawSunrise)
+ return WeatherService.weather.sunrise || "";
+ try {
+ const date = new Date(WeatherService.weather.rawSunrise);
+ const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ return date.toLocaleTimeString(Qt.locale(), format);
+ } catch (e) {
+ return WeatherService.weather.sunrise || "";
+ }
+ }
+
+ readonly property string sunsetTimeText: {
+ if (!WeatherService.weather.rawSunset)
+ return WeatherService.weather.sunset || "";
+ try {
+ const date = new Date(WeatherService.weather.rawSunset);
+ const format = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP";
+ return date.toLocaleTimeString(Qt.locale(), format);
+ } catch (e) {
+ return WeatherService.weather.sunset || "";
+ }
+ }
+
+ readonly property var heroMetrics: {
+ SettingsData.useFahrenheit;
+ SettingsData.windSpeedUnit;
+ return [
+ {
+ "icon": "humidity_low",
+ "label": I18n.tr("Humidity"),
+ "value": WeatherService.formatPercent(WeatherService.weather.humidity) ?? "--"
+ },
+ {
+ "icon": "air",
+ "label": I18n.tr("Wind"),
+ "value": WeatherService.formatSpeed(WeatherService.weather.wind) ?? "--"
+ },
+ {
+ "icon": "speed",
+ "label": I18n.tr("Pressure"),
+ "value": WeatherService.formatPressure(WeatherService.weather.pressure) ?? "--"
+ },
+ {
+ "icon": "rainy",
+ "label": I18n.tr("Precipitation"),
+ "value": (WeatherService.weather.precipitationProbability ?? 0) + "%"
+ },
+ {
+ "icon": "wb_twilight",
+ "label": I18n.tr("Sunrise"),
+ "value": root.sunriseTimeText || "--"
+ },
+ {
+ "icon": "bedtime",
+ "label": I18n.tr("Sunset"),
+ "value": root.sunsetTimeText || "--"
+ }
+ ];
+ }
+
+ Column {
+ id: unavailableColumn
+ anchors.centerIn: parent
+ spacing: Theme.spacingL
+ visible: !root.available
+
+ DankIcon {
+ name: "cloud_off"
+ size: Theme.iconSize * 2
+ color: Theme.withAlpha(Theme.surfaceText, 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: Theme.withAlpha(Theme.surfaceText, 0.7)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankIcon {
+ id: refreshButtonTwo
+ name: "refresh"
+ size: Theme.iconSize - 4
+ color: Theme.withAlpha(Theme.surfaceText, 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: 300
+ repeat: false
+ onTriggered: {
+ refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
+ }
+ }
+
+ onEntered: {
+ hoverDelayTwo.restart();
+ }
+
+ onExited: {
+ hoverDelayTwo.stop();
+ refreshButtonTooltipTwo.hide();
+ }
+
+ onClicked: {
+ refreshButtonTwo.isRefreshing = true;
+ WeatherService.forceRefresh();
+ refreshTimerTwo.restart();
+ }
+ }
+
+ DankTooltipV2 {
+ 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 {
+ id: mainColumn
+ anchors.fill: parent
+ visible: root.available
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: heroCard
+ width: parent.width
+ height: heroContent.height + Theme.spacingL * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Theme.withAlpha(Theme.outline, 0.08)
+ border.width: 1
+
+ Column {
+ id: heroContent
+ x: Theme.spacingL
+ y: Theme.spacingL
+ width: parent.width - Theme.spacingL * 2
+ spacing: Theme.spacingM
+
+ Item {
+ width: parent.width
+ height: Math.max(heroLeft.height, heroMetricsGrid.height)
+
+ Row {
+ id: heroLeft
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingL
+
+ DankIcon {
+ id: weatherIcon
+ name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
+ size: Theme.iconSize * 2
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+
+ layer.enabled: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
+ shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
+ }
+ }
+
+ Column {
+ id: tempColumn
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ Item {
+ anchors.left: parent.left
+ width: tempText.width + unitText.width + Theme.spacingXS
+ height: tempText.height
+
+ StyledText {
+ id: tempText
+ LayoutMirroring.enabled: false
+ text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
+ font.pixelSize: Theme.fontSizeXLarge + 8
+ color: Theme.surfaceText
+ font.weight: Font.Light
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ id: unitText
+ LayoutMirroring.enabled: false
+ text: SettingsData.useFahrenheit ? "F" : "C"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.withAlpha(Theme.surfaceText, 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.getWeatherCondition(WeatherService.weather.wCode)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.withAlpha(Theme.surfaceText, 0.7)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ property var feelsLike: SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)
+ text: I18n.tr("Feels Like %1°", "weather feels like temperature").arg(feelsLike)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ text: WeatherService.weather.city || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ visible: text.length > 0
+ anchors.left: parent.left
+ }
+ }
+ }
+
+ Grid {
+ id: heroMetricsGrid
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ columns: 3
+ columnSpacing: Theme.spacingXL
+ rowSpacing: Theme.spacingS
+
+ Repeater {
+ model: root.heroMetrics
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: modelData.icon
+ size: Theme.iconSizeSmall - 2
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: 2
+
+ StyledText {
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.withAlpha(Theme.surfaceText, 0.5)
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ text: modelData.value
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.left: parent.left
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: skyDateRow
+ width: parent.width
+ height: dateStepper.height
+
+ Item {
+ id: dateStepper
+ height: dateStepperInner.height + Theme.spacingM * 2
+ 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, SettingsData.use24HourClock ? "yyyy.MM.dd.HH.mm" : "yyyy.MM.dd.hh.mm.AP").split('.')
+
+ Item {
+ id: dateStepperInner
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ readonly property var space: Theme.spacingXS
+ width: yearStepper.width + monthStepper.width + dayStepper.width + hourStepper.width + minuteStepper.width + (suffix.visible ? suffix.width : 0) + 10.5 * space + 2 * dateStepperInnerPadding.width
+ height: Math.max(yearStepper.height, monthStepper.height, dayStepper.height, hourStepper.height, minuteStepper.height)
+
+ Item {
+ id: dateStepperInnerPadding
+ width: dateResetButton.width
+ }
+
+ DankNumberStepper {
+ id: yearStepper
+ anchors.left: dateStepperInnerPadding.right
+ anchors.leftMargin: parent.space
+ width: implicitWidth
+ text: dateStepper.splitDate[0]
+ }
+
+ 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: ":"
+ }
+ }
+ StyledText {
+ id: suffix
+ visible: !SettingsData.use24HourClock
+ anchors.verticalCenter: minuteStepper.verticalCenter
+ anchors.left: minuteStepper.right
+ anchors.leftMargin: 2 * parent.space
+ isMonospace: true
+ text: dateStepper.splitDate[5] ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ }
+ 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
+ anchors.left: dateStepper.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ height: parent.height
+
+ LayoutMirroring.enabled: false
+ LayoutMirroring.childrenInherit: true
+
+ 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, Theme.withAlpha(blackBlue(0.0), 0.8), Theme.withAlpha(blackBlue(0.2), 0.7), Theme.withAlpha(blackBlue(0.5), 0.6), Theme.withAlpha(blackBlue(0.7), 0.6), Theme.withAlpha(blackBlue(0.9), 0.6), Theme.withAlpha(blackBlue(1.0), 0.6), Theme.withAlpha(blackBlue(0.9), 0.6), Theme.withAlpha(blackBlue(0.7), 0.6), Theme.withAlpha(blackBlue(0.5), 0.6), Theme.withAlpha(blackBlue(0.2), 0.7), Theme.withAlpha(blackBlue(0.0), 0.8), blackColor, blackColor];
+ const index = periodIndex ?? 0;
+ return Theme.blend(colorMap[index], colorMap[index + 1], periodPercent ?? 0);
+ }
+ property var sunColor: {
+ const colorMap = [Theme.withAlpha(redColor, 0.05), Theme.withAlpha(redColor, 0.1), Theme.withAlpha(redColor, 0.3), Theme.withAlpha(redColor, 0.4), Theme.withAlpha(redColor, 0.5), Theme.withAlpha(blueColor, 0.2), Theme.withAlpha(blueColor, 0.0), Theme.withAlpha(blueColor, 0.2), Theme.withAlpha(redColor, 0.5), Theme.withAlpha(redColor, 0.4), Theme.withAlpha(redColor, 0.3), Theme.withAlpha(redColor, 0.1), Theme.withAlpha(redColor, 0.05), Theme.withAlpha(redColor, 0.0)];
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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
+ }
+
+ DankNFIcon {
+ id: moonPhase
+ name: WeatherService.getMoonPhase(skyBox.currentDate) || ""
+ size: Theme.fontSizeXLarge
+ 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: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
+ }
+ }
+
+ DankIcon {
+ id: sun
+ name: "light_mode"
+ size: Theme.fontSizeXLarge
+ 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: Theme.elevationEnabled
+ layer.effect: MultiEffect {
+ shadowEnabled: Theme.elevationEnabled
+ shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
+ shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
+ shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
+ blurMax: Theme.elevationBlurMax
+ shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
+ }
+ }
+ }
+ }
+
+ Item {
+ id: chipsRow
+ width: parent.width
+ height: forecastChips.height
+
+ DankFilterChips {
+ id: forecastChips
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ model: [I18n.tr("Daily"), I18n.tr("Hourly")]
+ currentIndex: root.showHourly ? 1 : 0
+ showCheck: false
+ showCounts: false
+ onSelectionChanged: index => {
+ root.showHourly = index === 1;
+ }
+ }
+
+ DankActionButton {
+ id: denseButton
+ anchors.right: refreshButton.left
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.showHourly && hourlyLoader.item !== null
+ iconName: SessionData.weatherHourlyDetailed ? "tile_large" : "tile_medium"
+ onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed)
+ }
+
+ DankIcon {
+ id: refreshButton
+ name: "refresh"
+ size: Theme.iconSize - 4
+ color: Theme.withAlpha(Theme.surfaceText, 0.4)
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ 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: 300
+ repeat: false
+ onTriggered: {
+ refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
+ }
+ }
+
+ onEntered: {
+ hoverDelay.restart();
+ }
+
+ onExited: {
+ hoverDelay.stop();
+ refreshButtonTooltip.hide();
+ }
+
+ onClicked: {
+ refreshButton.isRefreshing = true;
+ WeatherService.forceRefresh();
+ refreshTimer.restart();
+ }
+ }
+
+ DankTooltipV2 {
+ 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
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: root.height - heroCard.height - skyDateRow.height - chipsRow.height - mainColumn.spacing * 3
+
+ Loader {
+ id: dailyLoader
+ anchors.fill: parent
+ sourceComponent: dailyComponent
+ active: root.visible && root.available
+ visible: !root.showHourly
+ asynchronous: true
+ opacity: 0
+ onLoaded: {
+ root.syncing = true;
+ item.currentIndex = item.initialIndex;
+ item.positionViewAtIndex(item.initialIndex, ListView.SnapPosition);
+ root.syncing = false;
+ opacity = 1;
+ }
+ }
+
+ Loader {
+ id: hourlyLoader
+ anchors.fill: parent
+ sourceComponent: hourlyComponent
+ active: root.visible && root.available
+ visible: root.showHourly
+ 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
+ anchors.fill: parent
+ orientation: ListView.Horizontal
+ spacing: Theme.spacingS
+ clip: true
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlightMoveDuration: 0
+ interactive: true
+
+ property var cardHeight: height
+ 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: dense ? 10 : 5
+
+ 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;
+ }
+ }
+ }
+ }
+
+ Component {
+ id: dailyComponent
+ ListView {
+ id: dailyList
+ anchors.fill: parent
+ orientation: ListView.Horizontal
+ spacing: Theme.spacingS
+ clip: true
+ snapMode: ListView.SnapToItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlightMoveDuration: 0
+ interactive: true
+
+ property var cardHeight: height
+ 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;
+ }
+ }
+ }
+ }
+}