mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
* 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>
1111 lines
47 KiB
QML
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|