Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b11fe06276 | ||
|
|
33dd983463 | ||
|
|
b040eb8c47 | ||
|
|
bb9bed4184 | ||
|
|
9774d8d191 | ||
|
|
361a319d94 | ||
|
|
944b7587bc | ||
|
|
b0fbe8a14f | ||
|
|
d3641469b0 |
@@ -1,77 +1,113 @@
|
||||
# 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 styles
|
||||
|
||||
| Situatie | Weergave |
|
||||
Three display styles, configurable in the settings dialog:
|
||||
|
||||
| Style | Description |
|
||||
|---|---|
|
||||
| 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) | `--` |
|
||||
| *Plain text* | Trend-colored text (green/red/grey) |
|
||||
| *Split-flap board* | Airport board: white digit flaps, white € sign and a trend arrow in the default font. On a price change all flaps first reset to 0 (swept over ~2 s), then take their new values — the whole cycle spreads over the configured cascade time |
|
||||
| *Nixie tubes* | Retro amber glowing digits in glass tubes with configurable random flicker, white € sign and a trend arrow in the default font |
|
||||
|
||||
Formaat: `₿ 68.969 €` — duizendtallen volgens de systeemlocale, afgerond op hele euro's.
|
||||
In the split-flap and nixie styles a trend arrow sits right of the € sign:
|
||||
green ▲ when the price went up, red ▼ when it went down. When the price is
|
||||
unchanged the last arrow stays; on the very first reading (and on API errors)
|
||||
no arrow is shown. Display style changes apply immediately, without
|
||||
restarting the shell.
|
||||
|
||||
## Configuratie
|
||||
| Situation | Plain text color | Arrow in flip/nixie modes |
|
||||
|---|---|---|
|
||||
| Price up | green | green ▲ |
|
||||
| Price down | red | red ▼ |
|
||||
| Price unchanged | grey | last arrow stays |
|
||||
| First reading / API failure | grey / `--` | no arrow |
|
||||
|
||||
- **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.
|
||||
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.
|
||||
|
||||
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 1–15. 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 0–100. 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
|
||||
Dubbelklik op `dist/bitcoin-price-1.0.0.plasmoid` in Dolphin (Plasma installeert
|
||||
dan automatisch naar `~/.local/share/plasma/plasmoids/`).
|
||||
Configurable via right-click on the widget → *Configure Bitcoin Price…* → *General*.
|
||||
|
||||
## 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
|
||||
```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.3.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 <price>`,
|
||||
or `fetch failed, HTTP status: <code>`.
|
||||
|
||||
## 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)
|
||||
│ └── config.qml # config dialog registry (ConfigModel/ConfigCategory)
|
||||
├── 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
|
||||
└── configGeneral.qml # interval setting page
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
Binary file not shown.
Binary file not shown.
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 {
|
||||
ConfigCategory {
|
||||
name: i18n("Algemeen")
|
||||
name: i18n("General")
|
||||
icon: "preferences-desktop"
|
||||
source: "configGeneral.qml"
|
||||
}
|
||||
@@ -5,6 +5,27 @@
|
||||
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||
<kcfgfile name=""/>
|
||||
<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">
|
||||
<label>Refresh interval in minutes (minimum 5 because of the CoinGecko rate limit)</label>
|
||||
<default>5</default>
|
||||
|
||||
@@ -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 |
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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, followed by a trend
|
||||
* arrow (green up / red down; unchanged keeps the last arrow).
|
||||
* On a price change the board first resets every flap to "0" (swept
|
||||
* over ~2 s), then takes the new values (swept over the remainder of
|
||||
* the cascade setting).
|
||||
* 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: "--"
|
||||
// 1 = up, -1 = down, 0 = unchanged -> keep showing the last arrow
|
||||
property int trendDir: 0
|
||||
property int lastDir: 0
|
||||
onTrendDirChanged: {
|
||||
if (trendDir !== 0) {
|
||||
lastDir = trendDir;
|
||||
}
|
||||
}
|
||||
// total seconds over which the reset-to-zero + new value cycle spreads
|
||||
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/arrow 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);
|
||||
|
||||
// Phase 1: every flap resets to "0", swept over ~2 s.
|
||||
// Phase 2: every flap takes its new value, swept over the
|
||||
// remainder of the total cascade time.
|
||||
var total = Math.max(1, flapSeconds) * 1000;
|
||||
var resetSpan = Math.min(2000, Math.floor(total * 2 / 3));
|
||||
var valueStart = resetSpan;
|
||||
var resetStep = n > 1 ? Math.floor(Math.max(0, resetSpan - flapDuration) / (n - 1)) : 0;
|
||||
var valueStep = n > 1 ? Math.floor(Math.max(0, total - valueStart - flapDuration) / (n - 1)) : 0;
|
||||
|
||||
for (var i = 0; i < n; i++) {
|
||||
var cell = rep.itemAt(i);
|
||||
var to = i < nc.length ? nc[i] : "";
|
||||
if (cell) {
|
||||
cell.schedule(to, i * resetStep, valueStart + i * valueStep);
|
||||
}
|
||||
}
|
||||
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 properties (not bindings) so the animations control
|
||||
// when the character actually changes
|
||||
property string ch: ""
|
||||
property string targetCh: ""
|
||||
property string animCh: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
ch = root.chars[index] !== undefined ? root.chars[index] : "";
|
||||
}
|
||||
|
||||
// schedule the two-phase cycle: reset to "0", then target
|
||||
function schedule(t, resetDelay, valueDelay) {
|
||||
targetCh = t;
|
||||
resetTimer.interval = Math.max(0, resetDelay);
|
||||
resetTimer.restart();
|
||||
valueTimer.interval = Math.max(0, valueDelay);
|
||||
valueTimer.restart();
|
||||
}
|
||||
|
||||
function startFlip(newCh) {
|
||||
if (newCh === ch) {
|
||||
return;
|
||||
}
|
||||
animCh = newCh;
|
||||
flipAnim.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resetTimer
|
||||
interval: 0
|
||||
onTriggered: cell.startFlip("0")
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: valueTimer
|
||||
interval: 0
|
||||
onTriggered: cell.startFlip(cell.targetCh)
|
||||
}
|
||||
|
||||
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.animCh
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// breathing room between the board and the currency sign
|
||||
Item {
|
||||
width: root.height * 0.22
|
||||
height: 1
|
||||
}
|
||||
|
||||
// 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.85
|
||||
}
|
||||
|
||||
// same breathing room between the euro sign and the arrow
|
||||
Item {
|
||||
width: root.height * 0.22
|
||||
height: 1
|
||||
}
|
||||
|
||||
// trend arrow: green up / red down; unchanged keeps the last arrow
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.lastDir > 0 ? "\u25B2" : "\u25BC"
|
||||
visible: root.digitsOnly.length > 0 && root.lastDir !== 0
|
||||
color: root.lastDir > 0 ? "#2ecc71" : "#e74c3c"
|
||||
font.family: Qt.application.font.family
|
||||
font.pixelSize: root.height * 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 plain text (default font, white), followed by a
|
||||
* trend arrow (green up / red down; unchanged keeps the last arrow).
|
||||
* Pure QtQuick: testable outside the plasmoid shell.
|
||||
*/
|
||||
|
||||
import QtQuick 2.15
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
// wired by the parent loader
|
||||
property string display: "--"
|
||||
// 1 = up, -1 = down, 0 = unchanged -> keep showing the last arrow
|
||||
property int trendDir: 0
|
||||
property int lastDir: 0
|
||||
onTrendDirChanged: {
|
||||
if (trendDir !== 0) {
|
||||
lastDir = trendDir;
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
// repeat: true keeps the cycle alive; each trigger reschedules
|
||||
// the next interval (intensity 40 -> ~4 s between flicks,
|
||||
// 100 -> ~1.5 s).
|
||||
Timer {
|
||||
id: flickTimer
|
||||
repeat: true
|
||||
running: root.flickerOn
|
||||
interval: 1500
|
||||
onTriggered: {
|
||||
flick.restart();
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
function scheduleNext() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// breathing room between the tubes and the currency sign
|
||||
Item {
|
||||
width: root.height * 0.22
|
||||
height: 1
|
||||
}
|
||||
|
||||
// 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.85
|
||||
}
|
||||
|
||||
// same breathing room between the euro sign and the arrow
|
||||
Item {
|
||||
width: root.height * 0.22
|
||||
height: 1
|
||||
}
|
||||
|
||||
// trend arrow: green up / red down; unchanged keeps the last arrow
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.lastDir > 0 ? "\u25B2" : "\u25BC"
|
||||
visible: root.digits.length > 0 && root.lastDir !== 0
|
||||
color: root.lastDir > 0 ? "#2ecc71" : "#e74c3c"
|
||||
font.family: Qt.application.font.family
|
||||
font.pixelSize: root.height * 0.9
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.Controls 2.15
|
||||
import QtQuick.Controls 2.15 as QQC2
|
||||
import org.kde.kirigami 2.5 as Kirigami
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+136
-41
@@ -1,14 +1,18 @@
|
||||
/*
|
||||
* 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).
|
||||
* Display modes: 0 = plain text (trend-colored), 1 = split-flap board,
|
||||
* 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
|
||||
@@ -20,44 +24,84 @@ Item {
|
||||
id: root
|
||||
|
||||
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"
|
||||
|
||||
// -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: {
|
||||
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
|
||||
@@ -70,22 +114,48 @@ Item {
|
||||
Item {
|
||||
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
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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; });
|
||||
if (item.trendColor !== undefined) {
|
||||
item.trendColor = Qt.binding(function () { return root.priceColor; });
|
||||
}
|
||||
if (item.trendDir !== undefined) {
|
||||
item.trendDir = Qt.binding(function () { return root.trend; });
|
||||
}
|
||||
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 +164,43 @@ Item {
|
||||
Item {
|
||||
id: fullRep
|
||||
|
||||
implicitWidth: PlasmaCore.Units.gridUnit * 14
|
||||
implicitHeight: PlasmaCore.Units.gridUnit * 7
|
||||
implicitWidth: PlasmaCore.Units.gridUnit * 16
|
||||
implicitHeight: PlasmaCore.Units.gridUnit * 10
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: PlasmaCore.Units.smallSpacing
|
||||
|
||||
Text {
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.displayText
|
||||
color: root.priceColor
|
||||
font.family: PlasmaCore.Theme.defaultFont.family
|
||||
font.pixelSize: PlasmaCore.Units.gridUnit * 2
|
||||
source: "../images/bitcoin.svg"
|
||||
sourceSize.width: PlasmaCore.Units.iconSizes.large
|
||||
sourceSize.height: PlasmaCore.Units.iconSizes.large
|
||||
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; });
|
||||
if (item.trendColor !== undefined) {
|
||||
item.trendColor = Qt.binding(function () { return root.priceColor; });
|
||||
}
|
||||
if (item.trendDir !== undefined) {
|
||||
item.trendDir = Qt.binding(function () { return root.trend; });
|
||||
}
|
||||
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 {
|
||||
@@ -141,14 +235,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();
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-4
@@ -1,8 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Bitcoin Price
|
||||
Name[nl]=Bitcoin-prijs
|
||||
Comment=Shows the bitcoin price in EUR, color-coded by trend
|
||||
Comment[nl]=Toont de bitcoinprijs in EUR, kleurcodering op trend
|
||||
Type=Service
|
||||
Icon=bitcoin
|
||||
|
||||
@@ -12,8 +10,8 @@ X-KDE-ServiceTypes=Plasma/Applet
|
||||
X-KDE-PluginInfo-Author=Arnold Cordewiner
|
||||
X-KDE-PluginInfo-Email=hermes@a14r.be
|
||||
X-KDE-PluginInfo-Name=nl.a14r.bitcoinprice
|
||||
X-KDE-PluginInfo-Version=1.0.0
|
||||
X-KDE-PluginInfo-Website=http://192.168.110.5:40004/hermes/plasma-bitcoin-price
|
||||
X-KDE-PluginInfo-Version=1.5.1
|
||||
X-KDE-PluginInfo-Website=https://gitea.a14r.be/hermes/plasma-bitcoin-price
|
||||
X-KDE-PluginInfo-Category=Utilities
|
||||
X-KDE-PluginInfo-License=MIT
|
||||
X-KDE-PluginInfo-EnabledByDefault=true
|
||||
|
||||
Reference in New Issue
Block a user