Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e9b94217e | ||
|
|
e4e848540c | ||
|
|
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.
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,225 @@
|
||||
/*
|
||||
* 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; bottom-aligned with the
|
||||
// price digits (bottomMargin tuned so the glyph bottom matches)
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.height * 0.02
|
||||
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.
|
||||
// Bottom-aligned with the euro sign and the digits.
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.height * 0.16
|
||||
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 * 0.9
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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; bottom-aligned with the
|
||||
// price digits (bottomMargin tuned so the glyph bottom matches)
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.height * -0.015
|
||||
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.
|
||||
// Bottom-aligned with the euro sign and the digits.
|
||||
Text {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.height * 0.10
|
||||
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.81
|
||||
}
|
||||
|
||||
// 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: Math.round(PlasmaCore.Units.iconSizes.smallMedium * 1.1)
|
||||
sourceSize.height: Math.round(PlasmaCore.Units.iconSizes.smallMedium * 1.1)
|
||||
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: Math.round(PlasmaCore.Units.iconSizes.large * 1.1)
|
||||
sourceSize.height: Math.round(PlasmaCore.Units.iconSizes.large * 1.1)
|
||||
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.6.0
|
||||
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