mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 09:14:01 -04:00
324 lines
14 KiB
QML
324 lines
14 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Shapes
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.DCommon.Widgets
|
|
import qs.Modules.DDash
|
|
import "WeatherVisuals.js" as Visuals
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property string widgetId: "humidity"
|
|
property var widgetOptions: ({})
|
|
readonly property var weather: WeatherService.weather
|
|
readonly property bool graphics: widgetOptions.graphics !== false
|
|
readonly property var reading: {
|
|
SettingsData.useFahrenheit;
|
|
SettingsData.windSpeedUnit;
|
|
switch (widgetId) {
|
|
case "humidity":
|
|
return {
|
|
label: I18n.tr("Humidity", "weather metric label"),
|
|
icon: "humidity_low",
|
|
value: WeatherService.formatPercent(weather.humidity),
|
|
level: weather.humidity,
|
|
detail: weather.dewPoint == null ? "" : I18n.tr("Dew point %1", "Weather dew point temperature").arg(WeatherService.formatTemp(weather.dewPoint))
|
|
};
|
|
case "wind":
|
|
return {
|
|
label: I18n.tr("Wind", "noun, weather wind speed metric label"),
|
|
icon: "air",
|
|
value: WeatherService.formatSpeed(weather.wind),
|
|
shape: "arrow",
|
|
detail: weather.windDirection == null ? "" : I18n.tr("From %1", "Weather wind origin, followed by a cardinal direction").arg(root.windDirection)
|
|
};
|
|
case "precipitation":
|
|
return {
|
|
label: I18n.tr("Precipitation", "weather precipitation probability metric label"),
|
|
icon: "rainy",
|
|
value: WeatherService.formatPercent(weather.precipitationProbability),
|
|
level: weather.precipitationProbability,
|
|
detail: I18n.tr("Today")
|
|
};
|
|
case "pressure":
|
|
return {
|
|
label: I18n.tr("Pressure", "weather atmospheric pressure metric label"),
|
|
icon: "speed",
|
|
value: WeatherService.formatPressure(weather.pressure, false),
|
|
shape: "circle",
|
|
detail: SettingsData.useFahrenheit ? "inHg" : "hPa"
|
|
};
|
|
case "visibility":
|
|
return {
|
|
label: I18n.tr("Visibility", "weather visibility distance metric", true),
|
|
icon: "visibility",
|
|
value: WeatherService.formatVisibility(weather.visibility),
|
|
shape: "cookie12"
|
|
};
|
|
case "uv":
|
|
return {
|
|
label: I18n.tr("UV Index"),
|
|
icon: "light_mode",
|
|
value: weather.uv == null ? null : String(weather.uv),
|
|
shape: "cookie7",
|
|
detail: root.uvCategories[root.uvCategory] ?? ""
|
|
};
|
|
}
|
|
return {};
|
|
}
|
|
readonly property var uvCategories: [I18n.tr("Low"), I18n.tr("Moderate", "uv index category"), I18n.tr("High"), I18n.tr("Very High"), I18n.tr("Extreme", "uv index category")]
|
|
readonly property int uvCategory: Visuals.uvCategory(weather.uv)
|
|
readonly property string windDirection: {
|
|
const directions = [I18n.tr("N", "North compass direction"), I18n.tr("NE", "Northeast compass direction"), I18n.tr("E", "East compass direction"), I18n.tr("SE", "Southeast compass direction"), I18n.tr("S", "South compass direction"), I18n.tr("SW", "Southwest compass direction"), I18n.tr("W", "West compass direction"), I18n.tr("NW", "Northwest compass direction")];
|
|
return directions[Visuals.directionIndex(weather.windDirection)] ?? "";
|
|
}
|
|
readonly property real shapeLevel: {
|
|
if (!graphics || !weather.available)
|
|
return 0;
|
|
const hours = (weather.hourlyForecast ?? []).slice(0, 24);
|
|
switch (widgetId) {
|
|
case "pressure":
|
|
return Visuals.relativeLevel(weather.pressure, hours.map(f => f.pressure), false);
|
|
}
|
|
return 1;
|
|
}
|
|
readonly property bool hasValue: weather.available && reading.value != null
|
|
readonly property string graphicDescription: {
|
|
if (!graphics || !hasValue)
|
|
return "";
|
|
switch (widgetId) {
|
|
case "uv":
|
|
return I18n.tr("The highlighted marker shows the UV category: low, moderate, high, very high, or extreme.");
|
|
case "wind":
|
|
if (Theme.shapeScale === 0)
|
|
return I18n.tr("The direction label shows where the wind comes from.");
|
|
return I18n.tr("The arrow points in the direction the wind is blowing. The label shows where it comes from.");
|
|
case "humidity":
|
|
return I18n.tr("The fill shows relative humidity. The badge shows the dew point temperature.");
|
|
case "precipitation":
|
|
return I18n.tr("The fill shows today's precipitation probability.");
|
|
case "pressure":
|
|
const values = (weather.hourlyForecast ?? []).slice(0, 24).map(f => f.pressure).filter(v => Number.isFinite(v));
|
|
if (values.length === 0)
|
|
return "";
|
|
const minimum = Math.min(...values);
|
|
const maximum = Math.max(...values);
|
|
if (minimum === maximum)
|
|
return I18n.tr("Today's pressure forecast is steady. The gauge stays at the midpoint.");
|
|
return I18n.tr("Today's forecast pressure: %1 to %2. The gauge shows the current reading within that range.", "weather pressure card description, %1 is lowest, %2 highest pressure").arg(WeatherService.formatPressure(minimum)).arg(WeatherService.formatPressure(maximum));
|
|
}
|
|
return "";
|
|
}
|
|
readonly property color foreground: toneColors.contentColor
|
|
readonly property color accent: toneColors.accentColor
|
|
readonly property bool leadingContent: widgetId === "humidity" || widgetId === "precipitation"
|
|
|
|
radius: graphics && reading.shape ? Theme.fullRadius(width, height) : Theme.cornerRadiusXL
|
|
color: graphics && widgetId === "uv" ? "transparent" : toneColors.surfaceColor
|
|
border.width: graphics && widgetId === "uv" ? 0 : Theme.layerOutlineWidth
|
|
border.color: Theme.outlineMedium
|
|
Accessible.role: Accessible.StaticText
|
|
Accessible.name: reading.label + " " + (hasValue ? reading.value : I18n.tr("Not available")) + " " + (reading.detail ?? "")
|
|
Accessible.description: graphicDescription
|
|
|
|
DTone {
|
|
id: toneColors
|
|
tone: root.widgetOptions.tone ?? ""
|
|
}
|
|
|
|
StateLayer {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.NoButton
|
|
cursorShape: Qt.ArrowCursor
|
|
stateColor: "transparent"
|
|
enableRipple: false
|
|
tooltipText: root.graphicDescription
|
|
}
|
|
|
|
DMaterialShape {
|
|
anchors.centerIn: parent
|
|
readonly property real aspectRatio: root.widgetId === "wind" && Theme.shapeScale > 0 ? 17 / 20 : 1
|
|
readonly property real artworkSize: Math.max(0, Math.min(parent.width, parent.height) - (root.widgetId === "uv" ? 0 : root.widgetId === "wind" ? Theme.spacingXS * 2 : Theme.spacingS * 2)) * (root.widgetId === "wind" && Theme.shapeScale > 0 ? rotationScaleForAspectRatio(aspectRatio) : 1)
|
|
width: artworkSize * aspectRatio
|
|
height: artworkSize
|
|
shape: root.reading.shape ?? "square"
|
|
color: root.widgetId === "uv" ? toneColors.surfaceColor : Theme.withAlpha(root.accent, Theme.stateLayerDrag)
|
|
trackColor: Theme.withAlpha(root.accent, Theme.stateLayerHover)
|
|
fillProgress: root.shapeLevel
|
|
visible: root.graphics && !!root.reading.shape && root.widgetId !== "pressure"
|
|
rotation: Theme.shapeScale > 0 && root.widgetId === "wind" && root.weather.windDirection != null ? (root.weather.windDirection + 180) % 360 : 0
|
|
}
|
|
|
|
Shape {
|
|
anchors.fill: parent
|
|
visible: root.graphics && root.hasValue && root.reading.level != null
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
strokeWidth: 0
|
|
strokeColor: "transparent"
|
|
fillColor: Theme.withAlpha(root.accent, Theme.stateLayerDrag)
|
|
PathSvg {
|
|
path: Visuals.levelPath(root.width, root.height, root.radius, (root.reading.level ?? 0) / 100, Theme.spacingS)
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: content
|
|
|
|
anchors.centerIn: parent
|
|
width: Math.min(parent.width, parent.height)
|
|
height: width
|
|
|
|
Shape {
|
|
id: gauge
|
|
|
|
anchors.fill: parent
|
|
visible: root.graphics && root.widgetId === "pressure"
|
|
preferredRendererType: Shape.CurveRenderer
|
|
readonly property real thickness: Theme.spacingS
|
|
readonly property real inset: Theme.spacingS + thickness / 2
|
|
|
|
ShapePath {
|
|
strokeColor: Theme.withAlpha(root.accent, Theme.stateLayerHover)
|
|
strokeWidth: gauge.thickness
|
|
capStyle: Theme.shapeScale > 0 ? ShapePath.RoundCap : ShapePath.FlatCap
|
|
fillColor: "transparent"
|
|
PathSvg {
|
|
path: Visuals.pressurePath(content.width, gauge.inset, 1, Theme.shapeScale === 0)
|
|
}
|
|
}
|
|
|
|
ShapePath {
|
|
strokeColor: root.hasValue && root.shapeLevel > 0 ? root.accent : "transparent"
|
|
strokeWidth: gauge.thickness
|
|
capStyle: Theme.shapeScale > 0 ? ShapePath.RoundCap : ShapePath.FlatCap
|
|
fillColor: "transparent"
|
|
PathSvg {
|
|
path: Visuals.pressurePath(content.width, gauge.inset, root.shapeLevel, Theme.shapeScale === 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: heading
|
|
|
|
x: root.leadingContent ? (I18n.isRtl ? parent.width - width - Theme.spacingL : Theme.spacingL) : (parent.width - width) / 2
|
|
anchors.bottom: value.top
|
|
anchors.bottomMargin: Theme.spacingM
|
|
width: Math.min(implicitWidth, parent.width - Theme.spacingXL * 2)
|
|
spacing: Theme.spacingXS
|
|
|
|
DIcon {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
name: root.reading.icon ?? "cloud_off"
|
|
size: Theme.iconSize
|
|
color: root.foreground
|
|
}
|
|
|
|
StyledText {
|
|
width: Math.min(implicitWidth, content.width - Theme.spacingXL * 2 - Theme.iconSize - heading.spacing)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.reading.label ?? ""
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Theme.fontWeightMedium
|
|
color: root.foreground
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: value
|
|
|
|
anchors.centerIn: parent
|
|
width: Math.max(0, parent.width - Theme.spacingL * 2)
|
|
height: Theme.fontSizeDisplay + Theme.spacingS
|
|
text: root.hasValue ? root.reading.value : "--"
|
|
font.pixelSize: Theme.fontSizeDisplay
|
|
minimumPixelSize: Theme.fontSizeLarge
|
|
fontSizeMode: Text.HorizontalFit
|
|
font.weight: Theme.fontWeightMedium
|
|
color: root.foreground
|
|
horizontalAlignment: root.leadingContent ? Text.AlignLeft : Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
font.features: ({
|
|
"tnum": 1
|
|
})
|
|
}
|
|
|
|
StyledText {
|
|
anchors.top: value.bottom
|
|
anchors.topMargin: Theme.spacingS
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: value.width
|
|
visible: text !== "" && !dewPoint.visible
|
|
text: root.hasValue ? (root.reading.detail ?? "") : I18n.tr("Not available")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: root.foreground
|
|
horizontalAlignment: root.leadingContent ? Text.AlignLeft : Text.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
Repeater {
|
|
model: root.graphics && root.widgetId === "uv" ? root.uvCategories.length : 0
|
|
|
|
Rectangle {
|
|
required property int index
|
|
readonly property bool selected: root.hasValue && root.uvCategory === index
|
|
readonly property real angle: (150 - (I18n.isRtl ? 4 - index : index) * 30) * Math.PI / 180
|
|
readonly property real orbit: content.width / 2 - Theme.spacingXL
|
|
readonly property var categoryColors: [Theme.success, Theme.warning, Theme.warning, Theme.error, Theme.tertiary]
|
|
|
|
x: content.width / 2 + Math.cos(angle) * orbit - width / 2
|
|
y: content.height / 2 + Math.sin(angle) * orbit - height / 2
|
|
width: selected ? Theme.spacingL : Theme.spacingM
|
|
height: width
|
|
radius: Theme.fullRadius(width, height)
|
|
color: selected ? categoryColors[index] : Theme.withAlpha(categoryColors[index], Theme.stateLayerDrag)
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: dewPoint
|
|
|
|
x: I18n.isRtl ? parent.width - width - Theme.spacingL : Theme.spacingL
|
|
anchors.top: value.bottom
|
|
anchors.topMargin: Theme.spacingS
|
|
spacing: Theme.spacingS
|
|
visible: root.widgetId === "humidity" && root.hasValue && root.weather.dewPoint != null
|
|
|
|
Rectangle {
|
|
id: dewBadge
|
|
|
|
width: Math.max(Theme.iconButtonSize, dewValue.implicitWidth + Theme.spacingM)
|
|
height: Theme.iconButtonSize
|
|
radius: Theme.fullRadius(width, height)
|
|
color: toneColors.tinted ? toneColors.contentColor : Theme.primaryContainer
|
|
|
|
StyledText {
|
|
id: dewValue
|
|
|
|
anchors.centerIn: parent
|
|
text: WeatherService.formatTemp(root.weather.dewPoint) ?? "--"
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Theme.fontWeightMedium
|
|
color: toneColors.tinted ? toneColors.containerColor : Theme.onPrimaryContainer
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
width: Math.max(0, value.width - dewBadge.width - dewPoint.spacing)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: I18n.tr("Dew point")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: root.foreground
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
}
|