From 800c16b519e1e44ddb9397e3584de077ca7a2da1 Mon Sep 17 00:00:00 2001 From: Thomas Egli <38955561+thegli@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:48:49 +0200 Subject: [PATCH] yfquotes@thegli: Add individual quote design feature (#1261) - Implement new feature for individual design of each quote - Changes in quotes list are not instantly applied anymore - Update default value for User-Agent - Correct minor translation flaws - Ignore case on sorting quotes list --- yfquotes@thegli/README.md | 45 ++++ .../files/yfquotes@thegli/desklet.js | 221 ++++++++++++------ .../files/yfquotes@thegli/metadata.json | 2 +- .../files/yfquotes@thegli/po/ca.po | 47 ++-- .../files/yfquotes@thegli/po/da.po | 34 +-- .../files/yfquotes@thegli/po/de.po | 48 ++-- .../files/yfquotes@thegli/po/es.po | 34 +-- .../files/yfquotes@thegli/po/fi.po | 34 +-- .../files/yfquotes@thegli/po/hu.po | 34 +-- .../files/yfquotes@thegli/po/it.po | 34 +-- .../files/yfquotes@thegli/po/ko.po | 32 ++- .../files/yfquotes@thegli/po/nl.po | 34 +-- .../files/yfquotes@thegli/po/pt_BR.po | 34 +-- .../files/yfquotes@thegli/po/ro.po | 34 +-- .../files/yfquotes@thegli/po/ru.po | 34 +-- .../yfquotes@thegli/po/yfquotes@thegli.pot | 40 ++-- .../yfquotes@thegli/settings-schema.json | 12 +- 17 files changed, 495 insertions(+), 258 deletions(-) diff --git a/yfquotes@thegli/README.md b/yfquotes@thegli/README.md index a4aa3f1bb..b84935fe1 100644 --- a/yfquotes@thegli/README.md +++ b/yfquotes@thegli/README.md @@ -23,6 +23,36 @@ Either follow the installation instructions on [Cinnamon spices](https://cinnamo Check out the desklet configuration settings, and choose the data refresh period, the list of quotes to show, and quote details to display. The default list contains the Dow 30 companies. +### Individual Quote Design + +By default, all quotes in the list are rendered in the same style and color, following whatever settings are active and selected. +Optionally, the *name* and the *symbol* of each quote can have a custom design. These individual design settings override the global settings. They are configured with a set of text properties within the quotes list. + +The design properties are appended to the quote symbol in the format `property=value`. Each property/value pair is separated by a semicolon. Also insert a semicolon between the quote symbol and the first property. The order of the properties is irrelevant. +The full syntax: `SYMBOL;property1=value1;property2=value2` + +The following table explains all supported properties: + +| Property | Values | Description | +|---|---|---| +| color | any CSS color value | Text color for name and symbol | +| name | any text, except ';' | Custom name, overrides the original short/long name | +| style | normal, italic, oblique | Font style for name and symbol | +| weight | normal, bold, bolder, lighter, 100 - 900 | Font weight (thickness) for name and symbol | + +Some examples: + +``` +CAT;color=#f6d001;weight=500 +CSCO;name=Cisco;weight=bold;color=#00bceb +HD;color=#f96300 +MMM;name=Post-It Makers;color=#ff0000 +IBM;name=Big Blue;color=blue;weight=bolder +KO;name=Bubbly Brown Water;style=oblique;weight=lighter;color=#e61a27 +``` + +> Note that any changes in the quotes list are not applied immediately (anymore). Press the "Refresh quotes data" button to trigger a manual data update for the current quotes list. + ## Troubleshooting Problem: The desklet fails to load data, and shows error message "Status: 429 Too Many Requests". @@ -42,6 +72,21 @@ To disable the debug log mode, delete the "DEBUG" file, and restart the Cinnamon ## Release Notes +### 0.14.0 - August 21, 2024 + +Features: + +- style each quote individually - see section [Individual Quote Design](#individual-quote-design) for details +- add Catalan translation (courtesy of [Odyssey]((https://github.com/odyssey)) +- update Dutch translation (courtesy of [qadzek](https://github.com/qadzek)) +- update Hungarian translation (courtesy of [bossbob88](https://github.com/bossbob88)) +- update Spanish translation (courtesy of [haggen88](https://github.com/haggen88)) + +Bugfixes: + +- quotes list sorting is now case-insensitive +- changes in quotes list are not instantly applied anymore (preventing potential network congestion, and desktop instabilities) + ### 0.13.0 - July 10, 2024 Features: diff --git a/yfquotes@thegli/files/yfquotes@thegli/desklet.js b/yfquotes@thegli/files/yfquotes@thegli/desklet.js index 398e17126..f49953040 100644 --- a/yfquotes@thegli/files/yfquotes@thegli/desklet.js +++ b/yfquotes@thegli/files/yfquotes@thegli/desklet.js @@ -105,15 +105,26 @@ YahooFinanceQuoteUtils.prototype = { && object[property] !== null; }, - determineQuoteName: function(quote, useLongName) { + determineQuoteName: function(quote, symbolCustomization, useLongName) { + if (symbolCustomization.name !== null) { + return symbolCustomization.name; + } + if (useLongName && this.existsProperty(quote, "longName")) { return quote.longName; - } else if (this.existsProperty(quote, "shortName")) { + } + + if (this.existsProperty(quote, "shortName")) { return quote.shortName; } + return ABSENT; }, + buildSymbolList: function(symbolCustomizationMap) { + return Array.from(symbolCustomizationMap.keys()).join(); + }, + isOkStatus: function(soupMessage) { if (soupMessage) { if (IS_SOUP_2) { @@ -144,7 +155,7 @@ YahooFinanceQuoteUtils.prototype = { reason = "Too Many Requests"; } } - + return status + " " + reason; } } @@ -303,15 +314,15 @@ YahooFinanceQuoteReader.prototype = { } }, - getFinanceData: function(quoteSymbols, customUserAgent, callback) { + getFinanceData: function(symbolList, customUserAgent, callback) { const _that = this; - - if (quoteSymbols.join().length === 0) { + + if (symbolList.size === 0) { callback.call(_that, _that.buildErrorResponse(_("Empty quotes list. Open settings and add some symbols."))); return; } - - const requestUrl = this.createYahooQueryUrl(quoteSymbols); + + const requestUrl = this.createYahooQueryUrl(symbolList); const message = Soup.Message.new("GET", requestUrl); if (IS_SOUP_2) { @@ -354,8 +365,8 @@ YahooFinanceQuoteReader.prototype = { } }, - createYahooQueryUrl: function(quoteSymbols) { - const queryUrl = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=" + quoteSymbols.join() + "&crumb=" + _crumb; + createYahooQueryUrl: function(symbolList) { + const queryUrl = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=" + symbolList + "&crumb=" + _crumb; logDebug("YF query URL: " + queryUrl); return queryUrl; }, @@ -389,24 +400,26 @@ QuotesTable.prototype = { EQUALS: "\u25B6" }, - render: function(quotes, settings) { + render: function(quotes, symbolCustomizationMap, settings) { for (let rowIndex = 0, l = quotes.length; rowIndex < l; rowIndex++) { - this.renderTableRow(quotes[rowIndex], rowIndex, settings); + this.renderTableRow(quotes[rowIndex], symbolCustomizationMap, settings, rowIndex); } }, - renderTableRow: function(quote, rowIndex, settings) { + renderTableRow: function(quote, symbolCustomizationMap, settings, rowIndex) { let cellContents = []; + const symbol = quote.symbol; + const symbolCustomization = symbolCustomizationMap.get(symbol); if (settings.changeIcon) { cellContents.push(this.createPercentChangeIcon(quote, settings)); } if (settings.quoteName) { - cellContents.push(this.createQuoteLabel(this.quoteUtils.determineQuoteName(quote, settings.useLongName), - quote.symbol, settings.quoteLabelWidth, settings)); + cellContents.push(this.createQuoteLabel(this.quoteUtils.determineQuoteName(quote, symbolCustomization, settings.useLongName), + symbolCustomization, settings.quoteLabelWidth, settings)); } if (settings.quoteSymbol) { - cellContents.push(this.createQuoteLabel(quote.symbol, quote.symbol, settings.quoteSymbolWidth, settings)); + cellContents.push(this.createQuoteLabel(symbol, symbolCustomization, settings.quoteSymbolWidth, settings)); } if (settings.marketPrice) { cellContents.push(this.createMarketPriceLabel(quote, settings)); @@ -429,19 +442,19 @@ QuotesTable.prototype = { } }, - createQuoteLabel: function(labelText, quoteSymbol, width, settings) { + createQuoteLabel: function(labelText, symbolCustomization, width, settings) { const label = new St.Label({ text: labelText, style_class: "quotes-label", reactive: settings.linkQuote, - style: "width:" + width + "em; " + this.buildFontStyle(settings) + style: "width:" + width + "em; " + this.buildCustomStyle(settings, symbolCustomization) }); if (settings.linkQuote) { const symbolButton = new St.Button(); symbolButton.add_actor(label); symbolButton.connect("clicked", Lang.bind(this, function() { - Gio.app_info_launch_default_for_uri(YF_QUOTE_PAGE_URL + quoteSymbol, global.create_app_launch_context()); + Gio.app_info_launch_default_for_uri(YF_QUOTE_PAGE_URL + symbolCustomization.symbol, global.create_app_launch_context()); })); return symbolButton; } else { @@ -499,7 +512,7 @@ QuotesTable.prototype = { return new St.Label({ text: iconText, - style: this.buildColorAttribute(iconColor) + this.buildFontSizeAttribute(settings.fontSize) + style: this.buildColorAttribute(iconColor, null) + this.buildFontSizeAttribute(settings.fontSize) }); }, @@ -521,7 +534,7 @@ QuotesTable.prototype = { ? (this.roundAmount(quote.regularMarketChangePercent, 2, settings.strictRounding) + "%") : ABSENT, style_class: "quotes-number", - style: this.buildColorAttribute(labelColor) + this.buildFontSizeAttribute(settings.fontSize) + style: this.buildColorAttribute(labelColor, null) + this.buildFontSizeAttribute(settings.fontSize) }); }, @@ -582,17 +595,32 @@ QuotesTable.prototype = { style: this.buildFontStyle(settings) }); }, - + buildFontStyle(settings) { - return this.buildColorAttribute(settings.fontColor) + this.buildFontSizeAttribute(settings.fontSize); + return this.buildColorAttribute(settings.fontColor, null) + this.buildFontSizeAttribute(settings.fontSize); }, - - buildColorAttribute(color) { - return "color: " + color + "; "; + + buildCustomStyle(settings, symbolCustomization) { + return this.buildColorAttribute(settings.fontColor, symbolCustomization.color) + + this.buildFontSizeAttribute(settings.fontSize) + + this.buildFontStyleAttribute(symbolCustomization.style) + + this.buildFontWeightAttribute(symbolCustomization.weight) + }, + + buildColorAttribute(globalColor, symbolColor) { + return "color: " + (symbolColor !== null ? symbolColor : globalColor) + "; "; }, - + buildFontSizeAttribute(fontSize) { return fontSize > 0 ? "font-size: " + fontSize + "px; " : ""; + }, + + buildFontStyleAttribute(fontStyle) { + return "font-style: " + fontStyle + "; "; + }, + + buildFontWeightAttribute(fontWeight) { + return "font-weight: " + fontWeight + "; "; } }; @@ -603,6 +631,8 @@ function StockQuoteDesklet(metadata, id) { StockQuoteDesklet.prototype = { __proto__: Desklet.Desklet.prototype, + quoteUtils: new YahooFinanceQuoteUtils(), + init: function(metadata, id) { this.metadata = metadata; this.id = id; @@ -646,11 +676,10 @@ StockQuoteDesklet.prototype = { this.onSettingsChanged, null); this.settings.bindProperty(Settings.BindingDirection.IN, "customDateFormat", "customDateFormat", this.onSettingsChanged, null); - this.settings.bindProperty(Settings.BindingDirection.IN, "quoteSymbols", "quoteSymbolsText", - this.onSettingsChanged, null); + this.settings.bindProperty(Settings.BindingDirection.IN, "quoteSymbols", "quoteSymbolsText"); // no instant-refresh on change this.settings.bindProperty(Settings.BindingDirection.IN, "sortCriteria", "sortCriteria", this.onSettingsChanged, null); - this.settings.bindProperty(Settings.BindingDirection.IN, "sortDirection", "sortDirection", + this.settings.bindProperty(Settings.BindingDirection.IN, "sortDirection", "sortAscending", this.onSettingsChanged, null); this.settings.bindProperty(Settings.BindingDirection.IN, "showChangeIcon", "showChangeIcon", this.onSettingsChanged, null); @@ -690,7 +719,7 @@ StockQuoteDesklet.prototype = { this.onSettingsChanged, null); }, - getQuoteDisplaySettings: function(quotes) { + getQuoteDisplaySettings: function(quotes, symbolCustomizationMap) { return { "changeIcon": this.showChangeIcon, "quoteName": this.showQuoteName, @@ -715,7 +744,7 @@ StockQuoteDesklet.prototype = { "downtrendChangeColor": this.downtrendChangeColor, "unchangedTrendColor": this.unchangedTrendColor, "quoteSymbolWidth": Math.max.apply(Math, quotes.map((quote) => quote.symbol.length)), - "quoteLabelWidth": Math.max.apply(Math, quotes.map((quote) => this.quoteUtils.determineQuoteName(quote, this.useLongQuoteName).length)) / 2 + 2 + "quoteLabelWidth": Math.max.apply(Math, quotes.map((quote) => this.quoteUtils.determineQuoteName(quote, symbolCustomizationMap.get(quote.symbol), this.useLongQuoteName).length)) / 2 + 2 }; }, @@ -740,13 +769,13 @@ StockQuoteDesklet.prototype = { reactive: this.manualDataUpdate, style: "color: " + settings.fontColor + "; " + (settings.fontSize > 0 ? "font-size: " + settings.fontSize + "px;" : "") }); - + if (this.manualDataUpdate) { const updateButton = new St.Button(); updateButton.add_actor(label); updateButton.connect("clicked", Lang.bind(this, function() { - this.removeUpdateTimer(); - this.onUpdate(); + this.removeUpdateTimer(); + this.onUpdate(); })); return updateButton; } else { @@ -769,9 +798,9 @@ StockQuoteDesklet.prototype = { setBackground: function() { this.mainBox.style = "background-color: " + this.buildBackgroundColor(this.backgroundColor, this.transparency); }, - + buildBackgroundColor: function(rgbColorString, transparencyFactor) { - // parse RGB values between "rgb(...)" + // parse RGB values between "rgb(...)" const rgb = rgbColorString.match(/\((.*?)\)/)[1].split(","); return "rgba(" + parseInt(rgb[0]) + "," + parseInt(rgb[1]) + "," + parseInt(rgb[2]) + "," + transparencyFactor + ")"; }, @@ -782,24 +811,60 @@ StockQuoteDesklet.prototype = { this.onUpdate(); }, + onQuotesListChanged: function() { + this.removeUpdateTimer(); + this.onUpdate(); + }, + on_desklet_removed: function() { this.unrender(); this.removeUpdateTimer(); }, onUpdate: function() { - const quoteSymbols = this.quoteSymbolsText.trim().split("\n"); + const symbolCustomizationMap = this.buildSymbolCustomizationMap(this.quoteSymbolsText); const customUserAgent = this.sendCustomUserAgent ? this.customUserAgent : null; try { if (_crumb) { - this.renderFinanceData(quoteSymbols, customUserAgent); + this.renderFinanceData(symbolCustomizationMap, customUserAgent); } else { - this.fetchCookieAndRender(quoteSymbols, customUserAgent); + this.fetchCookieAndRender(symbolCustomizationMap, customUserAgent); } } catch (err) { - this.onError(quoteSymbols, err); + this.onError(this.quoteUtils.buildSymbolList(symbolCustomizationMap), err); + } + }, + + buildSymbolCustomizationMap: function(quoteSymbolsText) { + const symbolCustomizations = new Map(); + for (const line of quoteSymbolsText.trim().split("\n")) { + const customization = this.createSymbolCustomization(line) + symbolCustomizations.set(customization.symbol, customization); + } + logDebug("symbol customization map size: " + symbolCustomizations.size) + + return symbolCustomizations; + }, + + createSymbolCustomization: function(symbolLine) { + const lineParts = symbolLine.trim().split(";"); + + const customAttributes = new Map(); + for (const attr of lineParts.slice(1)) { + const [key, value] = attr.split('='); + if (key && value) { + customAttributes.set(key, value); + } } + + return { + symbol: lineParts[0], + name: customAttributes.has("name") ? customAttributes.get("name") : null, + style: customAttributes.has("style") ? customAttributes.get("style") : "normal", + weight: customAttributes.has("weight") ? customAttributes.get("weight") : "normal", + color: customAttributes.has("color") ? customAttributes.get("color") : null, + }; }, existsCookie: function(name) { @@ -813,23 +878,23 @@ StockQuoteDesklet.prototype = { return false; }, - fetchCookieAndRender: function(quoteSymbols, customUserAgent) { + fetchCookieAndRender: function(symbolCustomizationMap, customUserAgent) { const _that = this; this.quoteReader.getCookie(customUserAgent, function(authResponseMessage, responseBody) { logDebug("Cookie response body: " + responseBody); if (_that.existsCookie(AUTH_COOKIE)) { - _that.fetchCrumbAndRender(quoteSymbols, customUserAgent); + _that.fetchCrumbAndRender(symbolCustomizationMap, customUserAgent); } else if (_that.existsCookie(CONSENT_COOKIE)) { - _that.processConsentAndRender(authResponseMessage, responseBody, quoteSymbols, customUserAgent); + _that.processConsentAndRender(authResponseMessage, responseBody, symbolCustomizationMap, customUserAgent); } else { logWarning("Failed to retrieve auth cookie!"); - _that.renderErrorMessage(_("Failed to retrieve authorization parameter! Unable to fetch quotes data.\\nStatus: ") + _that.quoteUtils.getMessageStatusInfo(authResponseMessage)); + _that.renderErrorMessage(_("Failed to retrieve authorization parameter! Unable to fetch quotes data.\\nStatus: ") + _that.quoteUtils.getMessageStatusInfo(authResponseMessage), symbolCustomizationMap); } }); }, - processConsentAndRender: function(authResponseMessage, consentPage, quoteSymbols, customUserAgent) { + processConsentAndRender: function(authResponseMessage, consentPage, symbolCustomizationMap, customUserAgent) { const _that = this; const formElementRegex = /(