3 Commits
9 changed files with 391 additions and 36 deletions
+25 -13
View File
@@ -3,14 +3,25 @@
KDE Plasma 5 widget that shows the bitcoin price in EUR, fetched from the 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).
## Display ## Display styles
| Situation | Display | Three display styles, configurable in the settings dialog:
| Style | Description |
|---|---| |---|---|
| Price up compared to the previous reading | green | | *Plain text* | Trend-colored text (green/red/grey) |
| Price down compared to the previous reading | red | | *Split-flap board* | Airport board: white characters on black flaps, cascading flip animation on price changes, trend LED |
| Price unchanged (or first reading) | grey | | *Nixie tubes* | Retro amber glowing digits in glass tubes (with a soft flicker), trend LED |
| API failure (network error, 4xx/5xx, rate limit 429) | `--` |
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.
| 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 |
Format: `68,969 €` (thousands separators follow the system locale, rounded to Format: `68,969 €` (thousands separators follow the system locale, rounded to
whole euros) preceded by the bundled Bitcoin icon in the compact and full whole euros) preceded by the bundled Bitcoin icon in the compact and full
@@ -27,13 +38,13 @@ Configurable via right-click on the widget → *Configure Bitcoin Price…* →
## Installation ## Installation
### Graphical ### Graphical
Double-click `bitcoin-price-1.1.0.plasmoid` in Dolphin (Plasma installs it to Double-click `bitcoin-price-1.2.0.plasmoid` in Dolphin (Plasma installs it to
`~/.local/share/plasma/plasmoids/` automatically). The latest build is attached `~/.local/share/plasma/plasmoids/` automatically). The latest build is attached
to the [v1.1.0 release](../../releases/tag/v1.1.0). to the [v1.2.0 release](../../releases/tag/v1.2.0).
### Terminal ### Terminal
```bash ```bash
kpackagetool5 --type Plasma/Applet --upgrade bitcoin-price-1.1.0.plasmoid kpackagetool5 --type Plasma/Applet --upgrade bitcoin-price-1.2.0.plasmoid
# Then restart the shell if needed: # Then restart the shell if needed:
plasmashell --replace & plasmashell --replace &
``` ```
@@ -74,12 +85,13 @@ or `fetch failed, HTTP status: <code>`.
``` ```
metadata.desktop metadata.desktop
contents/ contents/
├── config/main.xml # config schema (refreshMinutes, min 5, max 1440) ├── config/
├── images/bitcoin.svg # bundled Bitcoin icon ├── 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/compare logic ├── main.qml # widget + fetch/compare logic
── config.qml # config dialog (categories) ── configGeneral.qml # interval setting page
└── configGeneral.qml # interval setting
``` ```
Repackage yourself: Repackage yourself:
Binary file not shown.
+9
View File
@@ -5,6 +5,15 @@
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="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>
+144
View File
@@ -0,0 +1,144 @@
/*
* Split-flap display mode — airport departure board style.
* White characters on black cells with a flap split line;
* changed characters flip with a cascading animation.
* 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"
spacing: height * 0.06
property var chars: []
property bool ready: false
readonly property real cellFont: height * 0.72
readonly property real cellWidth: cellFont * 0.80
Component.onCompleted: {
chars = display.split("");
ready = true;
}
onDisplayChanged: {
if (!ready) {
return;
}
var nc = display.split("");
var n = Math.max(chars.length, nc.length);
for (var i = 0; i < n; i++) {
var from = i < chars.length ? chars[i] : "";
var to = i < nc.length ? nc[i] : "";
if (from !== to) {
var cell = rep.itemAt(i);
if (cell) {
cell.flipTo(to, i * 45);
}
}
}
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
}
}
}
// trend LED (green/red/grey) — boards are monochrome, so the
// trend shows here instead of in the letter color
Rectangle {
width: root.height * 0.14
height: width
radius: width / 2
anchors.verticalCenter: parent.verticalCenter
color: root.trendColor
border.color: "#26262b"
border.width: 1
}
}
+138
View File
@@ -0,0 +1,138 @@
/*
* Nixie tube display mode — retro amber glowing digits in glass tubes.
* Glow is faked with layered text copies, so no QtGraphicalEffects
* dependency. 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"
spacing: height * 0.05
readonly property string digits: display.replace(/[^0-9]/g, "")
readonly property real tubeWidth: height * 0.52
readonly property real glowFont: height * 0.80
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
}
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
}
// occasional gentle nixie flicker
Timer {
interval: 2200 + Math.random() * 3200
repeat: false
running: root.visible && root.digits.length > 0
onTriggered: {
flick.stop();
flick.start();
interval = 2200 + Math.random() * 3200;
restart();
}
}
SequentialAnimation {
id: flick
NumberAnimation {
target: glowText
property: "opacity"
to: 0.82
duration: 70
}
NumberAnimation {
target: glowText
property: "opacity"
to: 1.0
duration: 140
}
}
}
}
}
// currency indicator, dim amber like an indicator lamp
Text {
anchors.verticalCenter: parent.verticalCenter
text: "\u20AC"
color: "#8a5a20"
font.family: "DejaVu Serif"
font.pixelSize: root.glowFont * 0.6
visible: root.digits.length > 0
}
// 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
}
+7
View File
@@ -12,12 +12,19 @@ Kirigami.FormLayout {
id: generalPage id: generalPage
// The cfg_ prefix is what Plasma uses to read/write plasmoid.configuration // 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_refreshMinutes: intervalSpin.value
Item { Item {
Kirigami.FormData.isSection: true 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 { QQC2.SpinBox {
id: intervalSpin id: intervalSpin
Kirigami.FormData.label: i18n("Refresh interval (minutes):") Kirigami.FormData.label: i18n("Refresh interval (minutes):")
+40 -22
View File
@@ -7,6 +7,10 @@
* - Grey : unchanged (or first reading) * - Grey : unchanged (or first reading)
* - "--" : API failure (network error, 4xx/5xx, 429 rate limit) * - "--" : API failure (network error, 4xx/5xx, 429 rate limit)
* *
* 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 * Refresh interval: configurable, minimum 5 minutes
* (CoinGecko limit: 1 call per 5 minutes). * (CoinGecko limit: 1 call per 5 minutes).
*/ */
@@ -20,6 +24,12 @@ 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)
// 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"
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 = down, 0 = unchanged / no previous reading yet, 1 = up // -1 = down, 0 = unchanged / no previous reading yet, 1 = up
@@ -61,8 +71,13 @@ Item {
} }
Component.onCompleted: { Component.onCompleted: {
console.log("[bitcoinprice] started, refresh interval:", try {
refreshMinutes, "minutes"); displayMode = Math.max(0, Math.min(2, 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
@@ -75,7 +90,9 @@ Item {
Item { Item {
id: compact id: compact
Layout.minimumWidth: row.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
Row { Row {
@@ -93,19 +110,16 @@ Item {
visible: root.state === "ok" visible: root.state === "ok"
} }
Text { Loader {
id: priceLabel id: displayLoader
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.displayText height: compact.height * 0.62
color: root.priceColor width: item ? item.implicitWidth : 0
textFormat: Text.PlainText source: root.modeSource
elide: Text.ElideRight onLoaded: {
horizontalAlignment: Text.AlignHCenter item.display = Qt.binding(function () { return root.displayText; });
verticalAlignment: Text.AlignVCenter item.trendColor = Qt.binding(function () { return root.priceColor; });
font.family: PlasmaCore.Theme.defaultFont.family }
font.pixelSize: compact.height > PlasmaCore.Units.iconSizes.small
? Math.round(compact.height * 0.6)
: PlasmaCore.Theme.defaultFont.pixelSize
} }
} }
} }
@@ -115,8 +129,8 @@ Item {
Item { Item {
id: fullRep id: fullRep
implicitWidth: PlasmaCore.Units.gridUnit * 14 implicitWidth: PlasmaCore.Units.gridUnit * 16
implicitHeight: PlasmaCore.Units.gridUnit * 9 implicitHeight: PlasmaCore.Units.gridUnit * 10
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -131,12 +145,16 @@ Item {
visible: root.state === "ok" visible: root.state === "ok"
} }
Text { Loader {
id: fullDisplayLoader
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: root.displayText height: PlasmaCore.Units.gridUnit * 2.4
color: root.priceColor width: item ? item.implicitWidth : 0
font.family: PlasmaCore.Theme.defaultFont.family source: root.modeSource
font.pixelSize: PlasmaCore.Units.gridUnit * 2 onLoaded: {
item.display = Qt.binding(function () { return root.displayText; });
item.trendColor = Qt.binding(function () { return root.priceColor; });
}
} }
Text { Text {
+1 -1
View File
@@ -10,7 +10,7 @@ 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.1.1 X-KDE-PluginInfo-Version=1.2.0
X-KDE-PluginInfo-Website=https://gitea.a14r.be/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