From 47f47c268c5f2cb21f3ba083ba49898c1d85c8cf Mon Sep 17 00:00:00 2001 From: Symeon Huang Date: Fri, 29 May 2020 22:40:24 +0100 Subject: [PATCH] First workable version --- build_plasmoid.sh | 16 ++++ plasmoid/contents/config/config.qml | 11 +++ plasmoid/contents/config/main.xml | 18 ++++ plasmoid/contents/ui/StockQuoteDelegate.qml | 50 ++++++++++ plasmoid/contents/ui/config/ConfigGeneral.qml | 56 +++++++++++ plasmoid/contents/ui/dataloader.mjs | 93 +++++++++++++++++++ plasmoid/contents/ui/main.qml | 86 +++++++++++++++++ plasmoid/metadata.desktop | 2 +- run_plasmoid.sh | 15 +++ 9 files changed, 346 insertions(+), 1 deletion(-) create mode 100755 build_plasmoid.sh create mode 100644 plasmoid/contents/config/config.qml create mode 100644 plasmoid/contents/config/main.xml create mode 100644 plasmoid/contents/ui/StockQuoteDelegate.qml create mode 100644 plasmoid/contents/ui/config/ConfigGeneral.qml create mode 100644 plasmoid/contents/ui/dataloader.mjs create mode 100755 run_plasmoid.sh diff --git a/build_plasmoid.sh b/build_plasmoid.sh new file mode 100755 index 0000000..38f68d8 --- /dev/null +++ b/build_plasmoid.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Based on the original script from https://github.com/Zren/plasma-applet-todolist/blob/master/build + +set -e + +PLASMOID_DIR=`dirname $0`/plasmoid +plasmoidName="com.librehat.yapstocks" +plasmoidVersion=$(grep "X-KDE-PluginInfo-Version" $PLASMOID_DIR/metadata.desktop | cut -d "=" -f 2) # kreadconfig5 doesn't work on my system somehow +filename="${plasmoidName}-v${plasmoidVersion}.plasmoid" +cd $PLASMOID_DIR +zip -r $filename * +cd - +mkdir -p dist +mv $PLASMOID_DIR/$filename dist/$filename +echo "md5: $(md5sum dist/$filename | awk '{ print $1 }')" +echo "sha256: $(sha256sum dist/$filename | awk '{ print $1 }')" diff --git a/plasmoid/contents/config/config.qml b/plasmoid/contents/config/config.qml new file mode 100644 index 0000000..26a3798 --- /dev/null +++ b/plasmoid/contents/config/config.qml @@ -0,0 +1,11 @@ +import QtQuick 2.12 + +import org.kde.plasma.configuration 2.0 + +ConfigModel { + ConfigCategory { + name: i18n("General") + icon: "configure" + source: "config/ConfigGeneral.qml" + } +} diff --git a/plasmoid/contents/config/main.xml b/plasmoid/contents/config/main.xml new file mode 100644 index 0000000..37b3b50 --- /dev/null +++ b/plasmoid/contents/config/main.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + 1800000 + + + + diff --git a/plasmoid/contents/ui/StockQuoteDelegate.qml b/plasmoid/contents/ui/StockQuoteDelegate.qml new file mode 100644 index 0000000..22ee95b --- /dev/null +++ b/plasmoid/contents/ui/StockQuoteDelegate.qml @@ -0,0 +1,50 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import org.kde.plasma.core 2.0 as PlasmaCore + +ColumnLayout { + spacing: -1 + + MenuSeparator { + Layout.fillWidth: true + visible: index != 0 + } + + RowLayout { + Layout.fillWidth: true + + Label { + text: symbol + font.weight: Font.Black + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + } + + Label { + text: currentPrice.toFixed(2) + Layout.alignment: Qt.AlignRight + } + + Label { + text: currency + Layout.alignment: Qt.AlignRight + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + text: exchangeName + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + } + + Text { + text: `${priceChange.toFixed(2)} (${priceChangePercentage.toFixed(2)}%)` + color: priceChange == 0 ? PlasmaCore.ColorScope.neutralTextColor : (priceChange > 0 ? PlasmaCore.ColorScope.positiveTextColor : PlasmaCore.ColorScope.negativeTextColor) + Layout.alignment: Qt.AlignRight + } + } +} diff --git a/plasmoid/contents/ui/config/ConfigGeneral.qml b/plasmoid/contents/ui/config/ConfigGeneral.qml new file mode 100644 index 0000000..c08bb43 --- /dev/null +++ b/plasmoid/contents/ui/config/ConfigGeneral.qml @@ -0,0 +1,56 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2.12 +import org.kde.plasma.plasmoid 2.0 + + +ColumnLayout { + id: root + + property var cfg_symbols: plasmoid.configuration.symbols + property int cfg_interval: plasmoid.configuration.updateInterval + + + RowLayout { + Layout.fillWidth: true + + Label { + text: i18n("Symbols:") + } + + TextField { + // TODO: Make this nicer by using a ListView + Layout.fillWidth: true + id: symbolsField + text: cfg_symbols.join(",") + placeholderText: "Yahoo! Finance symbols separated by comma ','" + onTextEdited: { + const symbols = text.split(",").map(sym => sym.trim()); + cfg_symbols = [...new Set(symbols)]; // Remove duplicates + } + } + } + + RowLayout { + Layout.fillWidth: true + Label { + text: i18n("Update every:") + } + + SpinBox { + id: updateIntervalSpin + from: 30 + to: 3600 + editable: true + textFromValue: (value) => i18np("%1 minute", "%1 minutes", value) + valueFromText: (text) => parseInt(text, 10) + + value: cfg_interval / 60000 + + onValueChanged: (value) => { + cfg_interval = value * 60000; + } + } + } +} diff --git a/plasmoid/contents/ui/dataloader.mjs b/plasmoid/contents/ui/dataloader.mjs new file mode 100644 index 0000000..5112df3 --- /dev/null +++ b/plasmoid/contents/ui/dataloader.mjs @@ -0,0 +1,93 @@ +/** + * Sends an HTTP request to the url + * @param {String} url + * @return {Promise} + */ +const HttpRequestP = (url) => { + const xhr = new XMLHttpRequest(); + return new Promise((resolve, reject) => { + xhr.onreadystatechange = () => { + if (xhr.readyState !== XMLHttpRequest.DONE) { + return; + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.responseText); + } else { + reject(xhr.statusText); + } + }; + xhr.onerror = reject; + xhr.open('GET', url, true); + xhr.send(); + }); +}; + +/** + * Resolves a security symbol from Yahoo Finance + * @param {String} symbol + * @return {Promise} + */ +const resolveSymbol = (symbol) => { + return HttpRequestP(`https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?symbol=${symbol}`) + .then((text) => { + const resp = JSON.parse(text); + if (resp.chart.error) { + throw new Error(resp.chart.error.description); + } + const meta = resp.chart.result[0].meta; + return { + symbol: meta.symbol, + currency: meta.currency, + instrument: meta.instrumentType, + exchangeName: meta.exchangeName, + currentPrice: meta.regularMarketPrice, + previousClose: meta.previousClose, + priceChange: meta.regularMarketPrice - meta.previousClose, + priceChangePercentage: (meta.regularMarketPrice - meta.previousClose) / meta.previousClose * 100, + updatedDateTime: new Date(meta.regularMarketTime * 1000), + exchange: { + timezone: meta.timezone, + timezoneName: meta.exchangeTimezoneName, + tradingPeriod: { + start: meta.currentTradingPeriod.regular.start, + end: meta.currentTradingPeriod.regular.end, + }, + }, + }; + }); +}; + +/** + * @param {Object} msg + * @param {String} msg.action "modify", "refresh" + * @param {String[]} [msg.symbols] symbols for action "modify" + * @param {ListModel} msg.model + */ +WorkerScript.onMessage = (msg) => { + return Promise.resolve().then(() => { + if (msg.action === "modify") { + msg.model.clear(); + return Promise.all(msg.symbols.map(resolveSymbol)).then((results) => { + results.forEach((result) => msg.model.append(result)); + msg.model.sync(); + }); + } + if (msg.action === "refresh") { + const promises = []; + const symbolIndexMap = new Map(); + for (let i = 0; i < msg.model.count; ++i) { + const symbol = msg.model.get(i).symbol; + promises.push(resolveSymbol(symbol)); + symbolIndexMap.set(symbol, i); + } + return Promise.all(promises).then((results) => { + results.forEach((result) => { + msg.model.set(symbolIndexMap.get(result.symbol), result); + }); + msg.model.sync(); + }); + } + }).catch((error) => { + console.log("Got an error", JSON.stringify(error)); + }).then(() => WorkerScript.sendMessage({})); +}; diff --git a/plasmoid/contents/ui/main.qml b/plasmoid/contents/ui/main.qml index d18466f..8734856 100644 --- a/plasmoid/contents/ui/main.qml +++ b/plasmoid/contents/ui/main.qml @@ -1,5 +1,91 @@ import QtQuick 2.12 +import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import QtQml.Models 2.12 +import org.kde.plasma.plasmoid 2.0 +import org.kde.plasma.core 2.0 as PlasmaCore +import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.plasma.extras 2.0 as PlasmaExtras Item { + property bool loading: false + property string lastUpdated + + readonly property var symbols: plasmoid.configuration.symbols + readonly property int updateInterval: plasmoid.configuration.updateInterval + + Plasmoid.icon: "labplot" // TODO: make an icon + + WorkerScript { + id: worker + source: "dataloader.mjs" + onMessage: { + loading = false; + lastUpdated = (new Date()).toLocaleString(); + timer.restart(); + } + } + + Timer { + id: timer + interval: updateInterval + running: true + repeat: true + // triggeredOnStart: true + onTriggered: { + if (symbolsModel.count > 0) { + loading = true; + worker.sendMessage({action: "refresh", model: symbolsModel}); + } + } + } + + onSymbolsChanged: { + loading = true; + worker.sendMessage({action: "modify", symbols: symbols, model: symbolsModel}); + } + + ScrollView { + anchors.fill: parent + + ListView { + model: ListModel { + id: symbolsModel + } + delegate: StockQuoteDelegate { + width: parent.width + } + spacing: PlasmaCore.Units.smallSpacing + + header: PlasmaExtras.Title { + text: "Stocks" + } + headerPositioning: ListView.OverlayHeader + + footer: RowLayout { + width: parent.width + PlasmaComponents.Label { + Layout.fillWidth: true + font.pointSize: 8 + font.underline: true + opacity: 0.7 + linkColor: PlasmaCore.ColorScope.textColor + text: "Powered by Yahoo! Finance" + onLinkActivated: Qt.openUrlExternally(link) + } + PlasmaComponents.Label { + Layout.alignment: Qt.AlignRight + font.pointSize: 8 + text: "Last Updated: " + lastUpdated + } + } + footerPositioning: ListView.OverlayFooter + } + } + + PlasmaComponents.BusyIndicator { + anchors.centerIn: parent + visible: loading + running: loading + } } diff --git a/plasmoid/metadata.desktop b/plasmoid/metadata.desktop index 5403f4a..855fc8a 100644 --- a/plasmoid/metadata.desktop +++ b/plasmoid/metadata.desktop @@ -3,7 +3,7 @@ Encoding=UTF-8 Name=YapStocks Comment=Yet Another Plasma Stocks Applet Type=Service -Icon= #FIXME +Icon=labplot X-KDE-ParentApp= X-KDE-PluginInfo-Author=Symeon Huang X-KDE-PluginInfo-Category=Education diff --git a/run_plasmoid.sh b/run_plasmoid.sh new file mode 100755 index 0000000..b47d91b --- /dev/null +++ b/run_plasmoid.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Based on https://github.com/Zren/plasma-applet-todolist/blob/master/run + +### Clear SVG cache +rm ~/.cache/plasma-svgelements-* + +killall plasmoidviewer + +SRC_PATH=`dirname $0` +export QML_DISABLE_DISK_CACHE=true + +plasmoidviewer -a "$SRC_PATH/plasmoid" -l topedge -f horizontal -x 0 -y 0 + +### Test French Locale +# LANG=fr_FR.UTF-8 plasmoidviewer -a "$SRC_PATH/plasmoid" -l topedge -f horizontal -x 0 -y 0