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 }