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