Warning, /plasma/kdeplasma-addons/applets/weather/package/contents/ui/main.qml is written in an unsupported language. File is not indexed.
0001 /* 0002 * SPDX-FileCopyrightText: 2018 Friedrich W. H. Kossebau <kossebau@kde.org> 0003 * SPDX-FileCopyrightText: 2023 Ismael Asensio <isma.af@gmail.com> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 import QtQuick 0009 0010 import org.kde.plasma.plasmoid 0011 import org.kde.plasma.core as PlasmaCore 0012 import org.kde.plasma.plasma5support as P5Support 0013 0014 import org.kde.plasma.private.weather 0015 0016 PlasmoidItem { 0017 id: root 0018 0019 Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground 0020 0021 readonly property bool inPanel: [ 0022 PlasmaCore.Types.TopEdge, 0023 PlasmaCore.Types.RightEdge, 0024 PlasmaCore.Types.BottomEdge, 0025 PlasmaCore.Types.LeftEdge, 0026 ].includes(Plasmoid.location) 0027 0028 readonly property string weatherSource: Plasmoid.configuration.source 0029 readonly property int updateInterval: Plasmoid.configuration.updateInterval 0030 readonly property int displayTemperatureUnit: Plasmoid.configuration.temperatureUnit 0031 readonly property int displaySpeedUnit: Plasmoid.configuration.speedUnit 0032 readonly property int displayPressureUnit: Plasmoid.configuration.pressureUnit 0033 readonly property int displayVisibilityUnit: Plasmoid.configuration.visibilityUnit 0034 0035 property int status: Util.Normal 0036 0037 readonly property int invalidUnit: -1 //TODO: make KUnitConversion::InvalidUnit usable here 0038 0039 // model providing final display strings for observation properties 0040 readonly property var observationModel: { 0041 const model = {}; 0042 const data = weatherDataSource.currentData || {}; 0043 0044 function getNumber(key) { 0045 const number = data[key]; 0046 if (typeof number === "string") { 0047 const parsedNumber = parseFloat(number); 0048 return isNaN(parsedNumber) ? null : parsedNumber; 0049 } 0050 return (typeof number !== "undefined") && (number !== "") ? number : null; 0051 } 0052 function getNumberOrString(key) { 0053 const number = data[key]; 0054 return (typeof number !== "undefined") && (number !== "") ? number : null; 0055 } 0056 0057 const reportTemperatureUnit = data["Temperature Unit"] || invalidUnit; 0058 const reportPressureUnit = data["Pressure Unit"] || invalidUnit; 0059 const reportVisibilityUnit = data["Visibility Unit"] || invalidUnit; 0060 const reportWindSpeedUnit = data["Wind Speed Unit"] || invalidUnit; 0061 0062 model["conditions"] = data["Current Conditions"] || ""; 0063 0064 const conditionIconName = data["Condition Icon"] || null; 0065 model["conditionIconName"] = conditionIconName ? Util.existingWeatherIconName(conditionIconName) : "weather-none-available"; 0066 0067 const temperature = getNumber("Temperature"); 0068 model["temperature"] = temperature !== null ? Util.temperatureToDisplayString(displayTemperatureUnit, temperature, reportTemperatureUnit, true, false) : ""; 0069 0070 const windchill = getNumber("Windchill"); 0071 // Use temperature unit to convert windchill temperature 0072 // we only show degrees symbol not actual temperature unit 0073 model["windchill"] = windchill !== null ? 0074 Util.temperatureToDisplayString(displayTemperatureUnit, windchill, reportTemperatureUnit, false, true) : 0075 ""; 0076 0077 const humidex = getNumber("Humidex"); 0078 // TODO: this seems wrong, does the humidex have temperature as units? 0079 // Use temperature unit to convert humidex temperature 0080 // we only show degrees symbol not actual temperature unit 0081 model["humidex"] = humidex !== null ? 0082 Util.temperatureToDisplayString(displayTemperatureUnit, humidex, reportTemperatureUnit, false, true) : 0083 ""; 0084 0085 const dewpoint = getNumber("Dewpoint"); 0086 model["dewpoint"] = dewpoint !== null ? 0087 Util.temperatureToDisplayString(displayTemperatureUnit, dewpoint, reportTemperatureUnit) : ""; 0088 0089 const pressure = getNumber("Pressure"); 0090 model["pressure"] = pressure !== null ? 0091 Util.valueToDisplayString(displayPressureUnit, pressure, reportPressureUnit, 2) : ""; 0092 0093 const pressureTendency = (data && data["Pressure Tendency"]) || null; 0094 model["pressureTendency"] = 0095 pressureTendency === "rising" ? i18nc("pressure tendency", "Rising") : 0096 pressureTendency === "falling" ? i18nc("pressure tendency", "Falling") : 0097 pressureTendency === "steady" ? i18nc("pressure tendency", "Steady") : 0098 /* else */ ""; 0099 0100 const visibility = getNumberOrString("Visibility"); 0101 model["visibility"] = visibility !== null ? 0102 ((reportVisibilityUnit !== invalidUnit) ? 0103 Util.valueToDisplayString(displayVisibilityUnit, visibility, reportVisibilityUnit, 1) : visibility) : 0104 ""; 0105 0106 const humidity = getNumber("Humidity"); 0107 model["humidity"] = humidity !== null ? Util.percentToDisplayString(humidity) : ""; 0108 0109 // TODO: missing check for windDirection validness 0110 const windDirection = data["Wind Direction"] || ""; 0111 const windSpeed = getNumberOrString("Wind Speed"); 0112 let windSpeedText; 0113 if (windSpeed !== null && windSpeed !== "") { 0114 const windSpeedNumeric = (typeof windSpeed !== 'number') ? parseFloat(windSpeed) : windSpeed; 0115 if (!isNaN(windSpeedNumeric)) { 0116 if (windSpeedNumeric !== 0) { 0117 windSpeedText = Util.valueToDisplayString(displaySpeedUnit, windSpeedNumeric, reportWindSpeedUnit, 1); 0118 } else { 0119 windSpeedText = i18nc("Wind condition", "Calm"); 0120 } 0121 } else { 0122 // TODO: i18n? 0123 windSpeedText = windSpeed; 0124 } 0125 } 0126 model["windSpeed"] = windSpeedText || ""; 0127 model["windDirectionId"] = windDirection; 0128 model["windDirection"] = windDirection ? i18nc("wind direction", windDirection) : ""; 0129 0130 const windGust = getNumber("Wind Gust"); 0131 model["windGust"] = windGust !== null ? Util.valueToDisplayString(displaySpeedUnit, windGust, reportWindSpeedUnit, 1) : ""; 0132 0133 return model; 0134 } 0135 0136 readonly property var generalModel: { 0137 const model = {}; 0138 const data = weatherDataSource.currentData || {}; 0139 0140 const todayForecastTokens = (data["Short Forecast Day 0"] || "").split("|"); 0141 0142 model["location"] = data["Place"] || ""; 0143 model["courtesy"] = data["Credit"] || ""; 0144 model["creditUrl"] = data["Credit Url"] || ""; 0145 0146 let forecastDayCount = parseInt(data["Total Weather Days"] || ""); 0147 0148 // We know EnvCan provides 13 items (7 day and 6 night) or 12 if starting with tonight's forecast 0149 const hasNightForecasts = weatherSource && weatherSource.split("|")[0] === "envcan" && forecastDayCount > 8; 0150 model["forecastNightRow"] = hasNightForecasts; 0151 if (hasNightForecasts) { 0152 model["forecastStartsAtNight"] = (forecastDayCount % 2 === 0); 0153 forecastDayCount = Math.ceil((forecastDayCount+1) / 2); 0154 } 0155 0156 const forecastTitle = (!isNaN(forecastDayCount) && forecastDayCount > 0) ? 0157 i18ncp("Forecast period timeframe", "1 Day", "%1 Days", forecastDayCount) : "" 0158 model["forecastTitle"] = forecastTitle; 0159 0160 let conditionIconName = observationModel.conditionIconName; 0161 if (!conditionIconName || conditionIconName === "weather-none-available") { 0162 // try icon from current weather forecast 0163 if (todayForecastTokens.length === 6 && todayForecastTokens[1] !== "N/U") { 0164 conditionIconName = Util.existingWeatherIconName(todayForecastTokens[1]); 0165 } else { 0166 conditionIconName = "weather-none-available"; 0167 } 0168 } 0169 model["currentConditionIconName"] = conditionIconName; 0170 0171 return model; 0172 } 0173 0174 readonly property var detailsModel: { 0175 const model = []; 0176 0177 if (observationModel.windchill) { 0178 model.push({ 0179 "label": i18nc("@label", "Windchill:"), 0180 "text": observationModel.windchill 0181 }); 0182 }; 0183 0184 if (observationModel.humidex) { 0185 model.push({ 0186 "label": i18nc("@label", "Humidex:"), 0187 "text": observationModel.humidex 0188 }); 0189 } 0190 0191 if (observationModel.dewpoint) { 0192 model.push({ 0193 "label": i18nc("@label ground temperature", "Dewpoint:"), 0194 "text": observationModel.dewpoint 0195 }); 0196 } 0197 0198 if (observationModel.pressure) { 0199 model.push({ 0200 "label": i18nc("@label", "Pressure:"), 0201 "text": observationModel.pressure 0202 }); 0203 } 0204 0205 if (observationModel.pressureTendency) { 0206 model.push({ 0207 "label": i18nc("@label pressure tendency, rising/falling/steady", "Pressure Tendency:"), 0208 "text": observationModel.pressureTendency 0209 }); 0210 } 0211 0212 if (observationModel.visibility) { 0213 model.push({ 0214 "label": i18nc("@label", "Visibility:"), 0215 "text": observationModel.visibility 0216 }); 0217 } 0218 0219 if (observationModel.humidity) { 0220 model.push({ 0221 "label": i18nc("@label", "Humidity:"), 0222 "text": observationModel.humidity 0223 }); 0224 } 0225 0226 if (observationModel.windGust) { 0227 model.push({ 0228 "label": i18nc("@label", "Wind Gust:"), 0229 "text": observationModel.windGust 0230 }); 0231 } 0232 0233 return model; 0234 } 0235 0236 readonly property var forecastModel: { 0237 const model = []; 0238 const data = weatherDataSource.currentData; 0239 0240 const forecastDayCount = parseInt((data && data["Total Weather Days"]) || ""); 0241 if (isNaN(forecastDayCount) || forecastDayCount <= 0) { 0242 return model; 0243 } 0244 0245 const reportTemperatureUnit = (data && data["Temperature Unit"]) || invalidUnit; 0246 0247 if (generalModel.forecastNightRow) { 0248 model.push({placeholder: i18nc("Time of the day (from the duple Day/Night)", "Day")}) 0249 model.push({placeholder: i18nc("Time of the day (from the duple Day/Night)", "Night")}) 0250 } 0251 0252 for (let i = 0; i < forecastDayCount; ++i) { 0253 const forecastInfo = { 0254 period: "", 0255 icon: "", 0256 condition: "", 0257 probability: "", 0258 tempHigh: "", 0259 tempLow: "", 0260 } 0261 0262 const forecastDayKey = "Short Forecast Day " + i; 0263 const forecastDayTokens = ((data && data[forecastDayKey]) || "").split("|"); 0264 if (forecastDayTokens.length !== 6) { 0265 // We don't have the right number of tokens, abort trying 0266 continue; 0267 } 0268 0269 // If the first item is a night forecast and we are showing them on second row, 0270 // add an empty placeholder 0271 if (i === 0 && generalModel.forecastNightRow && generalModel.forecastStartsAtNight) { 0272 model.push({placeholder: ""}) 0273 } 0274 0275 forecastInfo["period"] = forecastDayTokens[0]; 0276 0277 // If we see N/U (Not Used) we skip the item 0278 const weatherIconName = forecastDayTokens[1]; 0279 if (weatherIconName && weatherIconName !== "N/U") { 0280 forecastInfo["icon"] = Util.existingWeatherIconName(weatherIconName); 0281 forecastInfo["condition"] = forecastDayTokens[2]; 0282 0283 const probability = forecastDayTokens[5]; 0284 if (probability !== "N/U" && probability !== "N/A" && probability > 7.5) { 0285 forecastInfo["probability"] = Math.round(probability / 5 ) * 5; 0286 } 0287 } 0288 0289 const tempHigh = forecastDayTokens[3]; 0290 if (tempHigh !== "N/U" && tempHigh !== "N/A" && tempHigh) { 0291 forecastInfo["tempHigh"] = Util.temperatureToDisplayString(displayTemperatureUnit, tempHigh, reportTemperatureUnit, true); 0292 } 0293 0294 const tempLow = forecastDayTokens[4]; 0295 if (tempLow !== "N/U" && tempLow !== "N/A" && tempLow) { 0296 forecastInfo["tempLow"] = Util.temperatureToDisplayString(displayTemperatureUnit, tempLow, reportTemperatureUnit, true); 0297 } 0298 0299 model.push(forecastInfo); 0300 } 0301 0302 return model; 0303 } 0304 0305 readonly property var noticesModel: { 0306 const model = []; 0307 const data = weatherDataSource.currentData; 0308 0309 let warningsCount = parseInt((data && data["Total Warnings Issued"]) || ""); 0310 if (isNaN(warningsCount)) { 0311 warningsCount = 0; 0312 } 0313 for (let i = 0; i < warningsCount; ++i) { 0314 model.push({ 0315 'description': data[`Warning Description ${i}`], 0316 'infoUrl': data[`Warning Info ${i}`], 0317 'timestamp': data[`Warning Timestamp ${i}`], 0318 'priority': data[`Warning Priority ${i}`] ?? 0, 0319 }); 0320 } 0321 0322 return model; 0323 } 0324 0325 function symbolicizeIconName(iconName) { 0326 const symbolicSuffix = "-symbolic"; 0327 if (iconName.endsWith(symbolicSuffix)) { 0328 return iconName; 0329 } 0330 0331 return iconName + symbolicSuffix; 0332 } 0333 0334 P5Support.DataSource { 0335 id: weatherDataSource 0336 0337 readonly property var currentData: data[weatherSource] 0338 0339 engine: "weather" 0340 connectedSources: weatherSource 0341 interval: updateInterval * 60 * 1000 0342 onConnectedSourcesChanged: { 0343 if (weatherSource) { 0344 status = Util.Connecting 0345 connectionTimeoutTimer.start(); 0346 } 0347 } 0348 onCurrentDataChanged: { 0349 if (currentData) { 0350 status = Util.Normal 0351 connectionTimeoutTimer.stop(); 0352 } 0353 } 0354 } 0355 0356 Timer { 0357 id: connectionTimeoutTimer 0358 0359 interval: 60 * 1000 // 1 min 0360 repeat: false 0361 onTriggered: { 0362 status = Util.Timeout; 0363 } 0364 } 0365 0366 Plasmoid.icon: { 0367 let iconName; 0368 // workaround for now to ensure "Please configure" tooltip 0369 // TODO: remove when configurationRequired works 0370 if (status === Util.NeedsConfiguration) { 0371 iconName = "configure"; 0372 } else { 0373 iconName = generalModel.currentConditionIconName; 0374 } 0375 0376 if (inPanel) { 0377 iconName = symbolicizeIconName(iconName); 0378 } 0379 0380 return iconName; 0381 } 0382 Plasmoid.busy: status === Util.Connecting 0383 Plasmoid.configurationRequired: status === Util.NeedsConfiguration 0384 0385 toolTipMainText: (status === Util.NeedsConfiguration) ? 0386 i18nc("@info:tooltip %1 is the translated plasmoid name", "Click to configure %1", Plasmoid.title) : 0387 generalModel.location 0388 0389 toolTipSubText: { 0390 if (!generalModel.location) { 0391 return ""; 0392 } 0393 const tooltips = []; 0394 const temperature = Plasmoid.configuration.showTemperatureInTooltip ? observationModel.temperature : null; 0395 if (observationModel.conditions && temperature) { 0396 tooltips.push(i18nc("weather condition + temperature", 0397 "%1 %2", observationModel.conditions, temperature)); 0398 } else if (observationModel.conditions || temperature) { 0399 tooltips.push(observationModel.conditions || temperature); 0400 } 0401 if (Plasmoid.configuration.showWindInTooltip && observationModel.windSpeed) { 0402 if (observationModel.windDirection) { 0403 if (observationModel.windGust) { 0404 tooltips.push(i18nc("winddirection windspeed (windgust)", "%1 %2 (%3)", 0405 observationModel.windDirection, observationModel.windSpeed, observationModel.windGust)); 0406 } else { 0407 tooltips.push(i18nc("winddirection windspeed", "%1 %2", 0408 observationModel.windDirection, observationModel.windSpeed)); 0409 } 0410 } else { 0411 tooltips.push(observationModel.windSpeed); 0412 } 0413 } 0414 if (Plasmoid.configuration.showPressureInTooltip && observationModel.pressure) { 0415 if (observationModel.pressureTendency) { 0416 tooltips.push(i18nc("pressure (tendency)", "%1 (%2)", 0417 observationModel.pressure, observationModel.pressureTendency)); 0418 } else { 0419 tooltips.push(observationModel.pressure); 0420 } 0421 } 0422 if (Plasmoid.configuration.showHumidityInTooltip && observationModel.humidity) { 0423 tooltips.push(i18n("Humidity: %1", observationModel.humidity)); 0424 } 0425 0426 return tooltips.join("\n"); 0427 } 0428 0429 // Only exists because the default CompactRepresentation doesn't expose: 0430 // - Icon overlays, or a generic way to overlay something on top of the icon 0431 // - The ability to show text below or beside the icon 0432 // TODO remove once it gains those features. 0433 compactRepresentation: CompactRepresentation { 0434 generalModel: root.generalModel 0435 observationModel: root.observationModel 0436 } 0437 0438 fullRepresentation: FullRepresentation { 0439 generalModel: root.generalModel 0440 observationModel: root.observationModel 0441 } 0442 0443 Binding { 0444 target: Plasmoid 0445 property: "needsToBeSquare" 0446 value: (Plasmoid.containmentType & PlasmaCore.Types.CustomEmbeddedContainment) 0447 | (Plasmoid.containmentDisplayHints & PlasmaCore.Types.ContainmentForcesSquarePlasmoids) 0448 } 0449 0450 onWeatherSourceChanged: { 0451 if (weatherSource.length === 0) { 0452 status = Util.NeedsConfiguration 0453 } 0454 } 0455 0456 Component.onCompleted: weatherSourceChanged() 0457 }