From d3641469b0ede8898a6c6d9be444cdb88b021841 Mon Sep 17 00:00:00 2001 From: ArnoldCordewiner Date: Sat, 5 Sep 2026 20:07:12 +0200 Subject: [PATCH] v1.1.0: English UI + docs, bundled Bitcoin icon, canonical config page, gitea.a14r.be website URL, journal diagnostics --- README.md | 99 ++++++++++++++++------------- bitcoin-price-1.1.0.plasmoid | Bin 0 -> 5733 bytes contents/images/bitcoin.svg | 12 ++++ contents/ui/config.qml | 2 +- contents/ui/configGeneral.qml | 25 +++++++- contents/ui/main.qml | 101 +++++++++++++++++++----------- dist/bitcoin-price-1.0.0.plasmoid | Bin 0 -> 4256 bytes metadata.desktop | 6 +- 8 files changed, 160 insertions(+), 85 deletions(-) create mode 100644 bitcoin-price-1.1.0.plasmoid create mode 100644 contents/images/bitcoin.svg create mode 100644 dist/bitcoin-price-1.0.0.plasmoid diff --git a/README.md b/README.md index 17645b6..e83bc70 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,92 @@ -# plasma-bitcoin-price +# Bitcoin Price (Plasma 5 widget) -KDE Plasma 5 widget die de bitcoinprijs in EUR toont, opgehaald bij de +KDE Plasma 5 widget that shows the bitcoin price in EUR, fetched from the [CoinGecko API](https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur). -## Weergave +## Display -| Situatie | Weergave | +| Situation | Display | |---|---| -| Prijs gestegen t.o.v. vorige meting | groen | -| Prijs gedaald t.o.v. vorige meting | rood | -| Prijs gelijk gebleven (of eerste meting) | grijs | -| API-fout (netwerkfout, 4xx/5xx, rate limit 429) | `--` | +| Price up compared to the previous reading | green | +| Price down compared to the previous reading | red | +| Price unchanged (or first reading) | grey | +| API failure (network error, 4xx/5xx, rate limit 429) | `--` | -Formaat: `₿ 68.969 €` — duizendtallen volgens de systeemlocale, afgerond op hele euro's. +Format: `68,969 €` (thousands separators follow the system locale, rounded to +whole euros) preceded by the bundled Bitcoin icon in the compact and full +representations. -## Configuratie +## Configuration -- **Vernieuwinterval (minuten)** — standaard 5, minimaal **5**, maximaal 1440. - Het minimum van 5 minuten komt door de CoinGecko rate limit van 1 call per 5 minuten. +- **Refresh interval (minutes)** — default 5, minimum **5**, maximum 1440. + The 5-minute floor exists because the CoinGecko API allows one request per + 5 minutes. A hint text in the config dialog explains this. -Instelbaar via rechtsklik op de widget → *Configureer Bitcoin-prijs* → *Algemeen*. +Configurable via right-click on the widget → *Configure Bitcoin Price…* → *General*. -## Installatie +## Installation -### Grafisch -Dubbelklik op `dist/bitcoin-price-1.0.0.plasmoid` in Dolphin (Plasma installeert -dan automatisch naar `~/.local/share/plasma/plasmoids/`). +### Graphical +Double-click `bitcoin-price-1.1.0.plasmoid` in Dolphin (Plasma installs it to +`~/.local/share/plasma/plasmoids/` automatically). The latest build is attached +to the [v1.1.0 release](../../releases/tag/v1.1.0). ### Terminal ```bash -kpackagetool5 --type Plasma/Applet --install dist/bitcoin-price-1.0.0.plasmoid -# widget toevoegen via Add Widgets; bij twijfel plasma herladen: +kpackagetool5 --type Plasma/Applet --upgrade bitcoin-price-1.1.0.plasmoid +# Then restart the shell if needed: plasmashell --replace & ``` -Verwijderen: +Upgrading an already-installed widget requires `--upgrade` (`--install` fails +when the plugin id already exists). + +Uninstall: ```bash kpackagetool5 --type Plasma/Applet --remove nl.a14r.bitcoinprice ``` -## Notities +## Notes -- De API-URL gebruikt `vs_currencies=eur`. In de oorspronkelijke opdracht stond - een `usd`-URL, maar de eis was "prijs t.o.v. de euro" met een `€`-teken; een - dollarwaarde onder een €-teken zou foutief zijn. -- De trendvergelijking gebeurt tegen de laatst bekend geraakte prijs. Na een - API-fout blijft die referentie staan, zodat de kleur klopt zodra er weer - data binnenkomt. -- Zonder CoinGecko API-key kan de API ook tussendoor een 429 geven; de widget - toont dan `--` tot de eerstvolgende geslaagde poll. -- Getest op Debian 12 / Plasma 5 (Qt 5.15). Plasma 6 werkt vermoedelijk ook, - maar is niet getest. +- The API URL uses `vs_currencies=eur`. The original request referenced a + `usd` URL, but the requirement was "price versus the euro" with a €-sign; + showing a dollar amount under a € sign would be wrong. +- The trend comparison is made against the last known price. After an API + failure that reference is kept, so the color is correct again as soon as + fresh data arrives. +- Without a CoinGecko API key the API may occasionally return 429; the widget + shows `--` until the next successful poll. +- Tested on Plasma 5.27 / Qt 5.15. Plasma 6 is expected to work but is untested. -## Repository-layout +## Debugging + +The widget logs to the plasmashell journal: + +```bash +journalctl --user -u plasma-plasmashell.service | grep bitcoinprice +``` + +Expected lines: `started, refresh interval: N minutes`, `fetched EUR `, +or `fetch failed, HTTP status: `. + +## Repository layout ``` metadata.desktop contents/ -├── config/main.xml # config-schema (refreshMinutes, min 5, max 1440) +├── config/main.xml # config schema (refreshMinutes, min 5, max 1440) +├── images/bitcoin.svg # bundled Bitcoin icon └── ui/ - ├── main.qml # widget + fetch/vergelijk-logica - ├── config.qml # configuratievenster (categorieën) - └── configGeneral.qml # interval-instelling + ├── main.qml # widget + fetch/compare logic + ├── config.qml # config dialog (categories) + └── configGeneral.qml # interval setting ``` -Het kant-en-klare installatiebestand (`bitcoin-price-1.0.0.plasmoid`) staat als -bijlage bij de [v1.0.0-release](../../releases/tag/v1.0.0). - -Zelf (her)packagen: +Repackage yourself: ```bash zip -r bitcoin-price.plasmoid metadata.desktop contents ``` -## Licentie +## License -MIT — zie [LICENSE](LICENSE). +MIT — see [LICENSE](LICENSE). diff --git a/bitcoin-price-1.1.0.plasmoid b/bitcoin-price-1.1.0.plasmoid new file mode 100644 index 0000000000000000000000000000000000000000..103dcacd76c56c838dcd8fe2d89d731650b1ec72 GIT binary patch literal 5733 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 SkpKWb`s;=E;)54Y{{0^-%2~ny literal 0 HcmV?d00001 diff --git a/contents/images/bitcoin.svg b/contents/images/bitcoin.svg new file mode 100644 index 0000000..02a385e --- /dev/null +++ b/contents/images/bitcoin.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/contents/ui/config.qml b/contents/ui/config.qml index 51e0112..f9558d9 100644 --- a/contents/ui/config.qml +++ b/contents/ui/config.qml @@ -3,7 +3,7 @@ import org.kde.plasma.configuration 2.0 ConfigModel { ConfigCategory { - name: i18n("Algemeen") + name: i18n("General") icon: "preferences-desktop" source: "configGeneral.qml" } diff --git a/contents/ui/configGeneral.qml b/contents/ui/configGeneral.qml index 6da6a11..bfa6cee 100644 --- a/contents/ui/configGeneral.qml +++ b/contents/ui/configGeneral.qml @@ -1,16 +1,35 @@ +/* + * Config page for nl.a14r.bitcoinprice. + * Follows the canonical Plasma 5 config dialog pattern: + * Kirigami.FormLayout + cfg_ prefixed property aliases. + */ + import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick.Controls 2.15 as QQC2 import org.kde.kirigami 2.5 as Kirigami Kirigami.FormLayout { + id: generalPage + + // The cfg_ prefix is what Plasma uses to read/write plasmoid.configuration property alias cfg_refreshMinutes: intervalSpin.value - SpinBox { + Item { + Kirigami.FormData.isSection: true + } + + QQC2.SpinBox { id: intervalSpin + Kirigami.FormData.label: i18n("Refresh interval (minutes):") from: 5 to: 1440 stepSize: 5 editable: true - Kirigami.FormData.label: i18n("Vernieuwinterval (minuten):") + } + + QQC2.Label { + text: i18n("CoinGecko allows one request per 5 minutes; lower values are not possible.") + font.italic: true + opacity: 0.7 } } diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 53ebf79..c0da2a3 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -1,14 +1,14 @@ /* * nl.a14r.bitcoinprice — KDE Plasma 5 widget * - * Toont de bitcoinprijs in EUR (CoinGecko API). - * - Groen : prijs gestegen t.o.v. vorige meting - * - Rood : prijs gedaald t.o.v. vorige meting - * - Grijs : gelijk gebleven (of eerste meting) - * - "--" : API-fout (netwerkfout, 4xx/5xx, rate limit 429) + * Shows the bitcoin price in EUR (CoinGecko API). + * - Green : price up compared to the previous reading + * - Red : price down compared to the previous reading + * - Grey : unchanged (or first reading) + * - "--" : API failure (network error, 4xx/5xx, 429 rate limit) * - * Vernieuwinterval: configureerbaar, minimaal 5 minuten - * (CoinGecko limiet: 1 call per 5 minuten). + * Refresh interval: configurable, minimum 5 minutes + * (CoinGecko limit: 1 call per 5 minutes). */ import QtQuick 2.15 @@ -22,42 +22,47 @@ Item { readonly property int refreshMinutes: Math.max(5, plasmoid.configuration.refreshMinutes) readonly property string apiUrl: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur" - // -1 = gedaald, 0 = gelijk / nog geen vorige meting, 1 = gestegen + // -1 = down, 0 = unchanged / no previous reading yet, 1 = up property int trend: 0 - property double lastPrice: NaN // laatste succesvol opgehaalde prijs + property double lastPrice: NaN // last successfully fetched price property string state: "init" // init | ok | error - property string lastUpdated: "" // HH:mm:ss van laatste succesvolle update + property string lastUpdated: "" // HH:mm:ss of the last successful update readonly property string displayText: { if (state !== "ok" || isNaN(lastPrice)) { return "--"; } - return "\u20BF " + Number(lastPrice).toLocaleString(Qt.locale(), "f", 0) + " \u20AC"; + return Number(lastPrice).toLocaleString(Qt.locale(), "f", 0) + " \u20AC"; } readonly property color priceColor: { if (state !== "ok" || isNaN(lastPrice)) { - return PlasmaCore.Theme.disabledTextColor; // "--" bij fout/init + return PlasmaCore.Theme.disabledTextColor; // "--" on error/init } if (trend > 0) { - return "#2ecc71"; // groen + return "#2ecc71"; // green } if (trend < 0) { - return "#e74c3c"; // rood + return "#e74c3c"; // red } - return "#9e9e9e"; // grijs + return "#9e9e9e"; // grey } readonly property string statusText: { if (state === "error") { return lastUpdated === "" - ? i18n("API-fout") - : i18n("API-fout — laatste geldige prijs: %1", lastUpdated); + ? i18n("API error") + : i18n("API error — last known price: %1", lastUpdated); } if (lastUpdated === "") { - return i18n("Bezig met ophalen\u2026"); + return i18n("Fetching\u2026"); } - return i18n("Laatst bijgewerkt: %1", lastUpdated); + return i18n("Last updated: %1", lastUpdated); + } + + Component.onCompleted: { + console.log("[bitcoinprice] started, refresh interval:", + refreshMinutes, "minutes"); } Plasmoid.backgroundHints: PlasmaCore.Types.StandardBackground @@ -70,22 +75,38 @@ Item { Item { id: compact - Layout.minimumWidth: priceLabel.implicitWidth + PlasmaCore.Units.smallSpacing * 2 + Layout.minimumWidth: row.implicitWidth + PlasmaCore.Units.smallSpacing * 2 Layout.minimumHeight: PlasmaCore.Units.iconSizes.smallMedium - Text { - id: priceLabel + Row { + id: row anchors.centerIn: parent - text: root.displayText - color: root.priceColor - textFormat: Text.PlainText - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family: PlasmaCore.Theme.defaultFont.family - font.pixelSize: compact.height > PlasmaCore.Units.iconSizes.small - ? Math.round(compact.height * 0.6) - : PlasmaCore.Theme.defaultFont.pixelSize + spacing: PlasmaCore.Units.smallSpacing + + Image { + id: btcIcon + source: "../images/bitcoin.svg" + sourceSize.width: PlasmaCore.Units.iconSizes.smallMedium + sourceSize.height: PlasmaCore.Units.iconSizes.smallMedium + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + visible: root.state === "ok" + } + + Text { + id: priceLabel + anchors.verticalCenter: parent.verticalCenter + text: root.displayText + color: root.priceColor + textFormat: Text.PlainText + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.family: PlasmaCore.Theme.defaultFont.family + font.pixelSize: compact.height > PlasmaCore.Units.iconSizes.small + ? Math.round(compact.height * 0.6) + : PlasmaCore.Theme.defaultFont.pixelSize + } } } } @@ -95,12 +116,21 @@ Item { id: fullRep implicitWidth: PlasmaCore.Units.gridUnit * 14 - implicitHeight: PlasmaCore.Units.gridUnit * 7 + implicitHeight: PlasmaCore.Units.gridUnit * 9 Column { anchors.centerIn: parent spacing: PlasmaCore.Units.smallSpacing + Image { + anchors.horizontalCenter: parent.horizontalCenter + source: "../images/bitcoin.svg" + sourceSize.width: PlasmaCore.Units.iconSizes.large + sourceSize.height: PlasmaCore.Units.iconSizes.large + fillMode: Image.PreserveAspectFit + visible: root.state === "ok" + } + Text { anchors.horizontalCenter: parent.horizontalCenter text: root.displayText @@ -141,14 +171,15 @@ Item { try { var price = JSON.parse(xhr.responseText).bitcoin.eur; if (typeof price === "number" && !isNaN(price)) { + console.log("[bitcoinprice] fetched EUR", price); root.applyPrice(price); return; } } catch (e) { - // ongeldige JSON → val door naar de foutafhandeling + // invalid JSON → fall through to error handling } } - // API-fout (netwerk, 4xx/5xx, rate limit 429, onverwachte payload) + console.log("[bitcoinprice] fetch failed, HTTP status:", xhr.status); root.state = "error"; }; xhr.send(); diff --git a/dist/bitcoin-price-1.0.0.plasmoid b/dist/bitcoin-price-1.0.0.plasmoid new file mode 100644 index 0000000000000000000000000000000000000000..e87d85db77bc88270de878e31f6c5c4a38afe2dc GIT binary patch literal 4256 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 v