7 Commits
16 changed files with 674 additions and 92 deletions
+76 -42
View File
@@ -1,77 +1,111 @@
# 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). [CoinGecko API](https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur).
## Weergave ## Display styles
| Situatie | Weergave | Three display styles, configurable in the settings dialog:
| Style | Description |
|---|---| |---|---|
| Prijs gestegen t.o.v. vorige meting | groen | | *Plain text* | Trend-colored text (green/red/grey) |
| Prijs gedaald t.o.v. vorige meting | rood | | *Split-flap board* | Airport board: white digit flaps, white € sign in the default font, cascading flip animation on price changes (configurable duration), trend LED |
| Prijs gelijk gebleven (of eerste meting) | grijs | | *Nixie tubes* | Retro amber glowing digits in glass tubes with configurable random flicker, white € sign in the default font, trend LED |
| API-fout (netwerkfout, 4xx/5xx, rate limit 429) | `--` |
Formaat: `₿ 68.969 €` — duizendtallen volgens de systeemlocale, afgerond op hele euro's. In the split-flap and nixie styles the digits keep their display color and the
trend (up/down/unchanged) is shown on a small LED next to the display.
Display style changes apply immediately, without restarting the shell.
## Configuratie | Situation | Plain text color | Trend LED |
|---|---|---|
| Price up | green | green |
| Price down | red | red |
| Price unchanged / first reading | grey | grey |
| API failure (network error, 4xx/5xx, rate limit 429) | `--` | grey |
- **Vernieuwinterval (minuten)** — standaard 5, minimaal **5**, maximaal 1440. Format: `68,969 €` (thousands separators follow the system locale, rounded to
Het minimum van 5 minuten komt door de CoinGecko rate limit van 1 call per 5 minuten. whole euros) preceded by the bundled Bitcoin icon in the compact and full
representations.
Instelbaar via rechtsklik op de widget → *Configureer Bitcoin-prijs**Algemeen*. ## Configuration
## Installatie - **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.
- **Split-flap cascade (seconds)** — default 3, range 115. Spreads the flip
animation of a price change evenly over this many seconds (applies to the
split-flap display style). Only shown when that style is selected.
- **Nixie flicker (0 = off)** — default 40, range 0100. Random cathode-dropout
flicker of the nixie tubes; 0 disables it, higher values flicker more often
and deeper. Only shown when the nixie style is selected.
### Grafisch Configurable via right-click on the widget → *Configure Bitcoin Price…**General*.
Dubbelklik op `dist/bitcoin-price-1.0.0.plasmoid` in Dolphin (Plasma installeert
dan automatisch naar `~/.local/share/plasma/plasmoids/`). ## Installation
### Graphical
Double-click `bitcoin-price-1.3.0.plasmoid` in Dolphin (Plasma installs it to
`~/.local/share/plasma/plasmoids/` automatically). The latest build is attached
to the [v1.3.0 release](../../releases/tag/v1.2.0).
### Terminal ### Terminal
```bash ```bash
kpackagetool5 --type Plasma/Applet --install dist/bitcoin-price-1.0.0.plasmoid kpackagetool5 --type Plasma/Applet --upgrade bitcoin-price-1.3.0.plasmoid
# widget toevoegen via Add Widgets; bij twijfel plasma herladen: # Then restart the shell if needed:
plasmashell --replace & plasmashell --replace &
``` ```
Verwijderen: Upgrading an already-installed widget requires `--upgrade` (`--install` fails
when the plugin id already exists).
Uninstall:
```bash ```bash
kpackagetool5 --type Plasma/Applet --remove nl.a14r.bitcoinprice kpackagetool5 --type Plasma/Applet --remove nl.a14r.bitcoinprice
``` ```
## Notities ## Notes
- De API-URL gebruikt `vs_currencies=eur`. In de oorspronkelijke opdracht stond - The API URL uses `vs_currencies=eur`. The original request referenced a
een `usd`-URL, maar de eis was "prijs t.o.v. de euro" met een `€`-teken; een `usd` URL, but the requirement was "price versus the euro" with a €-sign;
dollarwaarde onder een €-teken zou foutief zijn. showing a dollar amount under a € sign would be wrong.
- De trendvergelijking gebeurt tegen de laatst bekend geraakte prijs. Na een - The trend comparison is made against the last known price. After an API
API-fout blijft die referentie staan, zodat de kleur klopt zodra er weer failure that reference is kept, so the color is correct again as soon as
data binnenkomt. fresh data arrives.
- Zonder CoinGecko API-key kan de API ook tussendoor een 429 geven; de widget - Without a CoinGecko API key the API may occasionally return 429; the widget
toont dan `--` tot de eerstvolgende geslaagde poll. shows `--` until the next successful poll.
- Getest op Debian 12 / Plasma 5 (Qt 5.15). Plasma 6 werkt vermoedelijk ook, - Tested on Plasma 5.27 / Qt 5.15. Plasma 6 is expected to work but is untested.
maar is niet getest.
## 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 <price>`,
or `fetch failed, HTTP status: <code>`.
## Repository layout
``` ```
metadata.desktop metadata.desktop
contents/ contents/
├── config/main.xml # config-schema (refreshMinutes, min 5, max 1440) ├── config/
│ ├── main.xml # config schema (refreshMinutes, min 5, max 1440)
│ └── config.qml # config dialog registry (ConfigModel/ConfigCategory)
├── images/bitcoin.svg # bundled Bitcoin icon
└── ui/ └── ui/
├── main.qml # widget + fetch/vergelijk-logica ├── main.qml # widget + fetch/compare logic
── config.qml # configuratievenster (categorieën) ── configGeneral.qml # interval setting page
└── configGeneral.qml # interval-instelling
``` ```
Het kant-en-klare installatiebestand (`bitcoin-price-1.0.0.plasmoid`) staat als Repackage yourself:
bijlage bij de [v1.0.0-release](../../releases/tag/v1.0.0).
Zelf (her)packagen:
```bash ```bash
zip -r bitcoin-price.plasmoid metadata.desktop contents zip -r bitcoin-price.plasmoid metadata.desktop contents
``` ```
## Licentie ## License
MIT — zie [LICENSE](LICENSE). MIT — see [LICENSE](LICENSE).
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@ import org.kde.plasma.configuration 2.0
ConfigModel { ConfigModel {
ConfigCategory { ConfigCategory {
name: i18n("Algemeen") name: i18n("General")
icon: "preferences-desktop" icon: "preferences-desktop"
source: "configGeneral.qml" source: "configGeneral.qml"
} }
+21
View File
@@ -5,6 +5,27 @@
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd"> http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile name=""/> <kcfgfile name=""/>
<group name="General"> <group name="General">
<entry name="displayMode" type="Enum">
<label>Display style for the price</label>
<choices>
<choice name="PlainText"/>
<choice name="SplitFlap"/>
<choice name="Nixie"/>
</choices>
<default>0</default>
</entry>
<entry name="flapSeconds" type="Int">
<label>Seconds over which the split-flap cascade spreads a price change</label>
<default>3</default>
<min>1</min>
<max>15</max>
</entry>
<entry name="nixieFlicker" type="Int">
<label>Nixie tube flicker intensity (0 = off, 100 = frequent and deep)</label>
<default>40</default>
<min>0</min>
<max>100</max>
</entry>
<entry name="refreshMinutes" type="Int"> <entry name="refreshMinutes" type="Int">
<label>Refresh interval in minutes (minimum 5 because of the CoinGecko rate limit)</label> <label>Refresh interval in minutes (minimum 5 because of the CoinGecko rate limit)</label>
<default>5</default> <default>5</default>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="6.82666in" height="6.82666in" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 6.82666 6.82666"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Layer_x0020_1">
<g id="_372402152">
<circle id="_372402800" fill="#FF9800" cx="3.41333" cy="3.41333" r="2.56"/>
<path id="_372402824" fill="#FFFFFE" d="M3.87171 3.18776c0.0803346,0 0.155882,-0.0312795 0.212665,-0.0880945 0.0568189,-0.0568465 0.0880945,-0.132335 0.0880945,-0.212665 0,-0.0803425 -0.0312756,-0.155886 -0.088122,-0.212701 -0.0567795,-0.0567913 -0.132303,-0.0880669 -0.212638,-0.0880669l-0.827106 0 0 0.601528 0.827106 0zm0.0249606 1.50341l0 0.225992 -0.451146 0 0 -0.225575 -0.200461 0 0 0.225575 -0.45115 0 0 -0.225575 -0.350843 0 0 -0.45115 0.150382 0 0 -1.6542 -0.150382 0 0 -0.45115 0.350843 0 0 -0.225575 0.45115 0 0 0.225575 0.200461 0 0 -0.225575 0.451146 0 0 0.225992c0.191488,0.00620866 0.37061,0.0837126 0.506701,0.219803 0.142051,0.142051 0.220244,0.330874 0.220244,0.531697 0,0.198319 -0.0762953,0.384929 -0.214953,0.526335 0.138697,0.141421 0.214953,0.328024 0.214953,0.526339 0,0.200823 -0.0782205,0.389646 -0.220217,0.531661 -0.136118,0.136122 -0.31524,0.213626 -0.506728,0.219835zm-0.852067 -0.450732l0.827106 0c0.0803346,0 0.155882,-0.0312835 0.212665,-0.0880906 0.0568189,-0.0568465 0.0880945,-0.132343 0.0880945,-0.212673 0,-0.0803346 -0.0312756,-0.155886 -0.088122,-0.212705 -0.0567795,-0.0567795 -0.132303,-0.088063 -0.212638,-0.088063l-0.827106 0 0 0.601531z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+186
View File
@@ -0,0 +1,186 @@
/*
* Split-flap display mode — airport departure board style.
* Only digits are shown as flaps; the euro sign is rendered as plain
* text (default font, white) next to the board, and the trend shows
* on a small LED. Changed characters flip with a cascading animation
* spread over the configured number of seconds.
* Pure QtQuick so it can be tested outside the plasmoid shell.
*/
import QtQuick 2.15
Row {
id: root
// wired by the parent loader
property string display: "--"
property color trendColor: "#9e9e9e"
// seconds over which the cascade to a new price is spread
property int flapSeconds: 3
spacing: height * 0.06
function digitsOf(s) {
var out = "";
for (var i = 0; i < s.length; i++) {
var ch = s.charAt(i);
if ((ch >= "0" && ch <= "9") || ch === "-") {
out += ch;
}
}
return out;
}
// UI-only derived values (euro visibility); logic must not rely on these —
// QML gives no ordering guarantee between a change handler and the
// re-evaluation of dependent bindings.
readonly property string digitsOnly: digitsOf(display)
readonly property string boardText: digitsOnly.length > 0 ? digitsOnly : "--"
property var chars: []
property bool ready: false
readonly property real cellFont: height * 0.72
readonly property real cellWidth: cellFont * 0.80
// one flap open+close takes 260 ms; keep delays above that
readonly property int flapDuration: 260
Component.onCompleted: {
chars = boardText.split("");
ready = true;
}
onDisplayChanged: {
if (!ready) {
return;
}
// compute from display directly: derived bindings may still be
// stale at this point (QML makes no ordering guarantee)
var digits = digitsOf(display);
var nc = (digits.length > 0 ? digits : "--").split("");
var n = Math.max(chars.length, nc.length);
// spread the changed cells evenly across flapSeconds
var changed = [];
for (var i = 0; i < n; i++) {
var from = i < chars.length ? chars[i] : "";
var to = i < nc.length ? nc[i] : "";
if (from !== to) {
changed.push([i, to]);
}
}
var span = Math.max(1, flapSeconds) * 1000 - flapDuration;
var step = changed.length > 1 ? Math.max(0, Math.floor(span / (changed.length - 1))) : 0;
for (var j = 0; j < changed.length; j++) {
var cell = rep.itemAt(changed[j][0]);
if (cell) {
cell.flipTo(changed[j][1], j * step);
}
}
chars = nc;
}
Repeater {
id: rep
model: root.chars.length
delegate: Rectangle {
id: cell
width: root.cellWidth
height: root.height
radius: 2
color: "#0b0b0d"
border.color: "#26262b"
border.width: 1
// plain property (not a binding) so the flip animation
// controls when the character actually changes
property string ch: ""
property string pendingCh: ""
Component.onCompleted: {
ch = root.chars[index] !== undefined ? root.chars[index] : "";
}
function flipTo(newCh, delay) {
if (newCh === ch) {
return;
}
pendingCh = newCh;
delayTimer.interval = Math.max(0, delay);
delayTimer.restart();
}
Timer {
id: delayTimer
interval: 0
onTriggered: flipAnim.start()
}
transform: Scale {
id: scaleTransform
origin.y: cell.height / 2
yScale: 1
}
SequentialAnimation {
id: flipAnim
NumberAnimation {
target: scaleTransform
property: "yScale"
to: 0.06
duration: 130
easing.type: Easing.InQuad
}
ScriptAction {
script: cell.ch = cell.pendingCh
}
NumberAnimation {
target: scaleTransform
property: "yScale"
to: 1
duration: 130
easing.type: Easing.OutQuad
}
}
Text {
anchors.centerIn: parent
text: cell.ch
color: "#f2f2f2"
font.family: "DejaVu Sans Mono"
font.pixelSize: root.cellFont
}
// the flap split line across the middle
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
y: parent.height / 2 - 1
width: parent.width
height: 2
color: "#000000"
opacity: 0.55
}
}
}
// euro sign in the plain-text font, white
Text {
anchors.verticalCenter: parent.verticalCenter
text: "\u20AC"
visible: root.digitsOnly.length > 0
color: "#ffffff"
font.family: Qt.application.font.family
font.pixelSize: root.height * 0.66
}
// trend LED (green/red/grey)
Rectangle {
width: root.height * 0.14
height: width
radius: width / 2
anchors.verticalCenter: parent.verticalCenter
color: root.trendColor
border.color: "#26262b"
border.width: 1
}
}
+170
View File
@@ -0,0 +1,170 @@
/*
* Nixie tube display mode — retro amber glowing digits in glass tubes.
* Glow is faked with layered text copies, so no QtGraphicalEffects
* dependency. Tubes flicker randomly (nixie cathode style); the
* flicker intensity is configurable (0 = off, 100 = nervous).
* The euro sign is rendered as plain text (default font, white).
* Pure QtQuick: testable outside the plasmoid shell.
*/
import QtQuick 2.15
Row {
id: root
// wired by the parent loader
property string display: "--"
property color trendColor: "#9e9e9e"
// 0 = no flicker, 100 = frequent and deep flicker
property int flickerIntensity: 40
spacing: height * 0.05
function digitsOf(s) {
var out = "";
for (var i = 0; i < s.length; i++) {
var ch = s.charAt(i);
if (ch >= "0" && ch <= "9") {
out += ch;
}
}
return out;
}
readonly property string digits: digitsOf(display)
readonly property real tubeWidth: height * 0.52
readonly property real glowFont: height * 0.80
readonly property bool flickerOn: flickerIntensity > 0 && digits.length > 0
Repeater {
model: root.digits.length
delegate: Rectangle {
id: tube
width: root.tubeWidth
height: root.height
radius: width * 0.30
gradient: Gradient {
GradientStop { position: 0.0; color: "#3a2410" }
GradientStop { position: 0.5; color: "#241505" }
GradientStop { position: 1.0; color: "#160c03" }
}
border.color: "#4a2e12"
border.width: 1
// glass reflection strip
Rectangle {
anchors.top: parent.top
anchors.topMargin: parent.height * 0.06
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width * 0.5
height: parent.height * 0.16
radius: height / 2
color: "#ffffff"
opacity: 0.06
}
// digit + glow layers, flickered as one group
Item {
id: glowGroup
anchors.fill: parent
Text {
id: glowText
anchors.centerIn: parent
text: root.digits[index]
color: "#ffb648"
font.family: "DejaVu Serif"
font.pixelSize: root.glowFont
// layered glow: two scaled copies behind the crisp digit
Text {
anchors.centerIn: parent
text: parent.text
color: "#ff8c00"
opacity: 0.30
font: parent.font
scale: 1.24
}
Text {
anchors.centerIn: parent
text: parent.text
color: "#ff9d1e"
opacity: 0.50
font: parent.font
scale: 1.10
}
}
}
// nixie flicker: random dropout of the cathode glow
Timer {
id: flickTimer
repeat: false
running: root.flickerOn
onTriggered: {
flick.restart();
scheduleNext();
}
function scheduleNext() {
// intensity 40 -> ~4 s between flicks, 100 -> ~1.5 s
var base = 6000 - root.flickerIntensity * 45;
interval = Math.max(700, base * (0.6 + Math.random() * 0.8));
}
Component.onCompleted: scheduleNext()
}
SequentialAnimation {
id: flick
ScriptAction {
script: {
var range = root.flickerIntensity / 100 * 0.75;
glowGroup.opacity = 1 - (0.15 + Math.random() * range);
}
}
PauseAnimation { duration: 40 + Math.random() * 140 }
NumberAnimation {
target: glowGroup
property: "opacity"
to: 1.0
duration: 90 + Math.random() * 160
easing.type: Easing.OutQuad
}
}
}
}
// euro sign in the plain-text font, white
Text {
anchors.verticalCenter: parent.verticalCenter
text: "\u20AC"
visible: root.digits.length > 0
color: "#ffffff"
font.family: Qt.application.font.family
font.pixelSize: root.height * 0.60
}
// error / empty state
Text {
anchors.verticalCenter: parent.verticalCenter
text: "--"
color: "#7a4e1c"
font.family: "DejaVu Serif"
font.pixelSize: root.glowFont * 0.8
visible: root.digits.length === 0
}
// trend LED (green/red/grey)
Rectangle {
width: root.height * 0.11
height: width
radius: width / 2
anchors.verticalCenter: parent.verticalCenter
color: root.trendColor
border.color: "#4a2e12"
border.width: 1
}
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Plain text display mode — colored price text.
* Pure QtQuick on purpose: no Plasma imports, so it can be loaded
* and tested outside the plasmoid shell.
*/
import QtQuick 2.15
Text {
id: root
// wired by the parent loader: the formatted price string
property string display: "--"
// trend color (green/red/grey) from the widget root
property color trendColor: "#9e9e9e"
text: root.display
color: root.trendColor
textFormat: Text.PlainText
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: Qt.application.font.family
font.pixelSize: root.height > 0
? Math.round(root.height * 0.82)
: Qt.application.font.pixelSize
}
+53 -4
View File
@@ -1,16 +1,65 @@
/*
* Config page for nl.a14r.bitcoinprice.
* Follows the canonical Plasma 5 config dialog pattern:
* Kirigami.FormLayout + cfg_ prefixed property aliases.
* Mode-specific settings are only visible for the matching style.
*/
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15 as QQC2
import org.kde.kirigami 2.5 as Kirigami import org.kde.kirigami 2.5 as Kirigami
Kirigami.FormLayout { Kirigami.FormLayout {
property alias cfg_refreshMinutes: intervalSpin.value id: generalPage
SpinBox { // The cfg_ prefix is what Plasma uses to read/write plasmoid.configuration
property alias cfg_displayMode: modeCombo.currentIndex
property alias cfg_refreshMinutes: intervalSpin.value
property alias cfg_flapSeconds: flapSpin.value
property alias cfg_nixieFlicker: flickSpin.value
Item {
Kirigami.FormData.isSection: true
}
QQC2.ComboBox {
id: modeCombo
Kirigami.FormData.label: i18n("Display style:")
model: [i18n("Plain text"), i18n("Split-flap board"), i18n("Nixie tubes")]
}
QQC2.SpinBox {
id: flapSpin
visible: modeCombo.currentIndex === 1
Kirigami.FormData.label: i18n("Split-flap cascade (seconds):")
from: 1
to: 15
stepSize: 1
editable: true
}
QQC2.SpinBox {
id: flickSpin
visible: modeCombo.currentIndex === 2
Kirigami.FormData.label: i18n("Nixie flicker (0 = off):")
from: 0
to: 100
stepSize: 10
editable: true
}
QQC2.SpinBox {
id: intervalSpin id: intervalSpin
Kirigami.FormData.label: i18n("Refresh interval (minutes):")
from: 5 from: 5
to: 1440 to: 1440
stepSize: 5 stepSize: 5
editable: true 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
} }
} }
+126 -41
View File
@@ -1,14 +1,18 @@
/* /*
* nl.a14r.bitcoinprice — KDE Plasma 5 widget * nl.a14r.bitcoinprice — KDE Plasma 5 widget
* *
* Toont de bitcoinprijs in EUR (CoinGecko API). * Shows the bitcoin price in EUR (CoinGecko API).
* - Groen : prijs gestegen t.o.v. vorige meting * - Green : price up compared to the previous reading
* - Rood : prijs gedaald t.o.v. vorige meting * - Red : price down compared to the previous reading
* - Grijs : gelijk gebleven (of eerste meting) * - Grey : unchanged (or first reading)
* - "--" : API-fout (netwerkfout, 4xx/5xx, rate limit 429) * - "--" : API failure (network error, 4xx/5xx, 429 rate limit)
* *
* Vernieuwinterval: configureerbaar, minimaal 5 minuten * Display modes: 0 = plain text (trend-colored), 1 = split-flap board,
* (CoinGecko limiet: 1 call per 5 minuten). * 2 = nixie tubes. In the flip and nixie modes the trend shows on a
* small LED, the digits themselves stay white / amber.
*
* Refresh interval: configurable, minimum 5 minutes
* (CoinGecko limit: 1 call per 5 minutes).
*/ */
import QtQuick 2.15 import QtQuick 2.15
@@ -20,44 +24,84 @@ Item {
id: root id: root
readonly property int refreshMinutes: Math.max(5, plasmoid.configuration.refreshMinutes) readonly property int refreshMinutes: Math.max(5, plasmoid.configuration.refreshMinutes)
// Seconds over which the split-flap display cascades to a new price
readonly property int flapSeconds: Math.max(1, plasmoid.configuration.flapSeconds)
// Nixie tube flicker intensity (0 = off, 100 = frequent and deep)
readonly property int nixieFlicker: Math.max(0, Math.min(100, plasmoid.configuration.nixieFlicker))
// 0 = plain text, 1 = split-flap board, 2 = nixie tubes
property int displayMode: 0
readonly property string modeSource: displayMode === 1 ? "DisplayFlip.qml"
: displayMode === 2 ? "DisplayNixie.qml"
: "DisplayText.qml"
// Enum config values arrive either as an int index or as the choice
// name, depending on the Plasma version — accept both.
function modeFromConfig(v) {
if (typeof v === "number") {
return Math.max(0, Math.min(2, Math.round(v)));
}
if (v === "SplitFlap") {
return 1;
}
if (v === "Nixie") {
return 2;
}
return 0;
}
readonly property string apiUrl: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur" 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 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 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: { readonly property string displayText: {
if (state !== "ok" || isNaN(lastPrice)) { if (state !== "ok" || isNaN(lastPrice)) {
return "--"; return "--";
} }
return "\u20BF " + Number(lastPrice).toLocaleString(Qt.locale(), "f", 0) + " \u20AC"; return Number(lastPrice).toLocaleString(Qt.locale(), "f", 0) + " \u20AC";
} }
readonly property color priceColor: { readonly property color priceColor: {
if (state !== "ok" || isNaN(lastPrice)) { if (state !== "ok" || isNaN(lastPrice)) {
return PlasmaCore.Theme.disabledTextColor; // "--" bij fout/init return PlasmaCore.Theme.disabledTextColor; // "--" on error/init
} }
if (trend > 0) { if (trend > 0) {
return "#2ecc71"; // groen return "#2ecc71"; // green
} }
if (trend < 0) { if (trend < 0) {
return "#e74c3c"; // rood return "#e74c3c"; // red
} }
return "#9e9e9e"; // grijs return "#9e9e9e"; // grey
} }
readonly property string statusText: { readonly property string statusText: {
if (state === "error") { if (state === "error") {
return lastUpdated === "" return lastUpdated === ""
? i18n("API-fout") ? i18n("API error")
: i18n("API-fout — laatste geldige prijs: %1", lastUpdated); : i18n("API error — last known price: %1", lastUpdated);
} }
if (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: {
try {
// live binding: config changes from the dialog apply immediately
displayMode = Qt.binding(function () {
return modeFromConfig(plasmoid.configuration.displayMode);
});
} catch (e) {
// outside a real plasmoid shell (tests): keep the default
}
console.log("[bitcoinprice] started, refresh interval:", refreshMinutes,
"minutes, display mode:", displayMode);
} }
Plasmoid.backgroundHints: PlasmaCore.Types.StandardBackground Plasmoid.backgroundHints: PlasmaCore.Types.StandardBackground
@@ -70,22 +114,43 @@ Item {
Item { Item {
id: compact id: compact
Layout.minimumWidth: priceLabel.implicitWidth + PlasmaCore.Units.smallSpacing * 2 Layout.minimumWidth: btcIcon.width + row.spacing
+ (displayLoader.item ? displayLoader.item.implicitWidth : 0)
+ PlasmaCore.Units.smallSpacing * 2
Layout.minimumHeight: PlasmaCore.Units.iconSizes.smallMedium Layout.minimumHeight: PlasmaCore.Units.iconSizes.smallMedium
Text { Row {
id: priceLabel id: row
anchors.centerIn: parent anchors.centerIn: parent
text: root.displayText spacing: PlasmaCore.Units.smallSpacing
color: root.priceColor
textFormat: Text.PlainText Image {
elide: Text.ElideRight id: btcIcon
horizontalAlignment: Text.AlignHCenter source: "../images/bitcoin.svg"
verticalAlignment: Text.AlignVCenter sourceSize.width: PlasmaCore.Units.iconSizes.smallMedium
font.family: PlasmaCore.Theme.defaultFont.family sourceSize.height: PlasmaCore.Units.iconSizes.smallMedium
font.pixelSize: compact.height > PlasmaCore.Units.iconSizes.small anchors.verticalCenter: parent.verticalCenter
? Math.round(compact.height * 0.6) fillMode: Image.PreserveAspectFit
: PlasmaCore.Theme.defaultFont.pixelSize visible: root.state === "ok"
}
Loader {
id: displayLoader
anchors.verticalCenter: parent.verticalCenter
height: compact.height * 0.62
width: item ? item.implicitWidth : 0
source: root.modeSource
onLoaded: {
item.display = Qt.binding(function () { return root.displayText; });
item.trendColor = Qt.binding(function () { return root.priceColor; });
if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
}
if (item.flickerIntensity !== undefined) {
item.flickerIntensity = Qt.binding(function () { return root.nixieFlicker; });
}
}
}
} }
} }
} }
@@ -94,19 +159,38 @@ Item {
Item { Item {
id: fullRep id: fullRep
implicitWidth: PlasmaCore.Units.gridUnit * 14 implicitWidth: PlasmaCore.Units.gridUnit * 16
implicitHeight: PlasmaCore.Units.gridUnit * 7 implicitHeight: PlasmaCore.Units.gridUnit * 10
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: PlasmaCore.Units.smallSpacing spacing: PlasmaCore.Units.smallSpacing
Text { Image {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: root.displayText source: "../images/bitcoin.svg"
color: root.priceColor sourceSize.width: PlasmaCore.Units.iconSizes.large
font.family: PlasmaCore.Theme.defaultFont.family sourceSize.height: PlasmaCore.Units.iconSizes.large
font.pixelSize: PlasmaCore.Units.gridUnit * 2 fillMode: Image.PreserveAspectFit
visible: root.state === "ok"
}
Loader {
id: fullDisplayLoader
anchors.horizontalCenter: parent.horizontalCenter
height: PlasmaCore.Units.gridUnit * 2.4
width: item ? item.implicitWidth : 0
source: root.modeSource
onLoaded: {
item.display = Qt.binding(function () { return root.displayText; });
item.trendColor = Qt.binding(function () { return root.priceColor; });
if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
}
if (item.flickerIntensity !== undefined) {
item.flickerIntensity = Qt.binding(function () { return root.nixieFlicker; });
}
}
} }
Text { Text {
@@ -141,14 +225,15 @@ Item {
try { try {
var price = JSON.parse(xhr.responseText).bitcoin.eur; var price = JSON.parse(xhr.responseText).bitcoin.eur;
if (typeof price === "number" && !isNaN(price)) { if (typeof price === "number" && !isNaN(price)) {
console.log("[bitcoinprice] fetched EUR", price);
root.applyPrice(price); root.applyPrice(price);
return; return;
} }
} catch (e) { } 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"; root.state = "error";
}; };
xhr.send(); xhr.send();
Binary file not shown.
+2 -4
View File
@@ -1,8 +1,6 @@
[Desktop Entry] [Desktop Entry]
Name=Bitcoin Price Name=Bitcoin Price
Name[nl]=Bitcoin-prijs
Comment=Shows the bitcoin price in EUR, color-coded by trend Comment=Shows the bitcoin price in EUR, color-coded by trend
Comment[nl]=Toont de bitcoinprijs in EUR, kleurcodering op trend
Type=Service Type=Service
Icon=bitcoin Icon=bitcoin
@@ -12,8 +10,8 @@ X-KDE-ServiceTypes=Plasma/Applet
X-KDE-PluginInfo-Author=Arnold Cordewiner X-KDE-PluginInfo-Author=Arnold Cordewiner
X-KDE-PluginInfo-Email=hermes@a14r.be X-KDE-PluginInfo-Email=hermes@a14r.be
X-KDE-PluginInfo-Name=nl.a14r.bitcoinprice X-KDE-PluginInfo-Name=nl.a14r.bitcoinprice
X-KDE-PluginInfo-Version=1.0.0 X-KDE-PluginInfo-Version=1.4.0
X-KDE-PluginInfo-Website=http://192.168.110.5:40004/hermes/plasma-bitcoin-price X-KDE-PluginInfo-Website=https://gitea.a14r.be/hermes/plasma-bitcoin-price
X-KDE-PluginInfo-Category=Utilities X-KDE-PluginInfo-Category=Utilities
X-KDE-PluginInfo-License=MIT X-KDE-PluginInfo-License=MIT
X-KDE-PluginInfo-EnabledByDefault=true X-KDE-PluginInfo-EnabledByDefault=true