v1.4.0: digit-only flaps with white euro text, nixie white euro + configurable flicker, mode-conditional config fields; fix stale derived-property reads in change handler

This commit is contained in:
ArnoldCordewiner
2026-09-05 20:51:05 +02:00
parent bb9bed4184
commit b040eb8c47
8 changed files with 154 additions and 60 deletions
+6 -3
View File
@@ -10,8 +10,8 @@ Three display styles, configurable in the settings dialog:
| Style | Description | | Style | Description |
|---|---| |---|---|
| *Plain text* | Trend-colored text (green/red/grey) | | *Plain text* | Trend-colored text (green/red/grey) |
| *Split-flap board* | Airport board: white characters on black flaps, cascading flip animation on price changes, trend LED | | *Split-flap board* | Airport board: white digit flaps, white € sign in the default font, cascading flip animation on price changes (configurable duration), trend LED |
| *Nixie tubes* | Retro amber glowing digits in glass tubes (with a soft flicker), trend LED | | *Nixie tubes* | Retro amber glowing digits in glass tubes with configurable random flicker, white € sign in the default font, trend LED |
In the split-flap and nixie styles the digits keep their display color and the 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. trend (up/down/unchanged) is shown on a small LED next to the display.
@@ -35,7 +35,10 @@ representations.
5 minutes. A hint text in the config dialog explains this. 5 minutes. A hint text in the config dialog explains this.
- **Split-flap cascade (seconds)** — default 3, range 115. Spreads the flip - **Split-flap cascade (seconds)** — default 3, range 115. Spreads the flip
animation of a price change evenly over this many seconds (applies to the animation of a price change evenly over this many seconds (applies to the
split-flap display style). 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.
Configurable via right-click on the widget → *Configure Bitcoin Price…**General*. Configurable via right-click on the widget → *Configure Bitcoin Price…**General*.
Binary file not shown.
+6
View File
@@ -20,6 +20,12 @@
<min>1</min> <min>1</min>
<max>15</max> <max>15</max>
</entry> </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>
+37 -6
View File
@@ -1,7 +1,9 @@
/* /*
* Split-flap display mode — airport departure board style. * Split-flap display mode — airport departure board style.
* White characters on black cells with a flap split line; * Only digits are shown as flaps; the euro sign is rendered as plain
* changed characters flip with a cascading animation. * 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. * Pure QtQuick so it can be tested outside the plasmoid shell.
*/ */
@@ -18,6 +20,23 @@ Row {
spacing: height * 0.06 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 var chars: []
property bool ready: false property bool ready: false
readonly property real cellFont: height * 0.72 readonly property real cellFont: height * 0.72
@@ -26,7 +45,7 @@ Row {
readonly property int flapDuration: 260 readonly property int flapDuration: 260
Component.onCompleted: { Component.onCompleted: {
chars = display.split(""); chars = boardText.split("");
ready = true; ready = true;
} }
@@ -34,7 +53,10 @@ Row {
if (!ready) { if (!ready) {
return; return;
} }
var nc = display.split(""); // 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); var n = Math.max(chars.length, nc.length);
// spread the changed cells evenly across flapSeconds // spread the changed cells evenly across flapSeconds
var changed = []; var changed = [];
@@ -141,8 +163,17 @@ Row {
} }
} }
// trend LED (green/red/grey) — boards are monochrome, so the // euro sign in the plain-text font, white
// trend shows here instead of in the letter color 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 { Rectangle {
width: root.height * 0.14 width: root.height * 0.14
height: width height: width
+82 -50
View File
@@ -1,7 +1,10 @@
/* /*
* Nixie tube display mode — retro amber glowing digits in glass tubes. * Nixie tube display mode — retro amber glowing digits in glass tubes.
* Glow is faked with layered text copies, so no QtGraphicalEffects * Glow is faked with layered text copies, so no QtGraphicalEffects
* dependency. Pure QtQuick: testable outside the plasmoid shell. * 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 import QtQuick 2.15
@@ -12,12 +15,26 @@ Row {
// wired by the parent loader // wired by the parent loader
property string display: "--" property string display: "--"
property color trendColor: "#9e9e9e" property color trendColor: "#9e9e9e"
// 0 = no flicker, 100 = frequent and deep flicker
property int flickerIntensity: 40
spacing: height * 0.05 spacing: height * 0.05
readonly property string digits: display.replace(/[^0-9]/g, "") 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 tubeWidth: height * 0.52
readonly property real glowFont: height * 0.80 readonly property real glowFont: height * 0.80
readonly property bool flickerOn: flickerIntensity > 0 && digits.length > 0
Repeater { Repeater {
model: root.digits.length model: root.digits.length
@@ -48,71 +65,86 @@ Row {
opacity: 0.06 opacity: 0.06
} }
Text { // digit + glow layers, flickered as one group
id: glowText Item {
anchors.centerIn: parent id: glowGroup
text: root.digits[index] anchors.fill: parent
color: "#ffb648"
font.family: "DejaVu Serif"
font.pixelSize: root.glowFont
// layered glow: two scaled copies behind the crisp digit
Text { Text {
id: glowText
anchors.centerIn: parent anchors.centerIn: parent
text: parent.text text: root.digits[index]
color: "#ff8c00" color: "#ffb648"
opacity: 0.30 font.family: "DejaVu Serif"
font: parent.font font.pixelSize: root.glowFont
scale: 1.24
// 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
}
} }
Text { }
anchors.centerIn: parent
text: parent.text // nixie flicker: random dropout of the cathode glow
color: "#ff9d1e" Timer {
opacity: 0.50 id: flickTimer
font: parent.font repeat: false
scale: 1.10 running: root.flickerOn
onTriggered: {
flick.restart();
scheduleNext();
} }
// occasional gentle nixie flicker function scheduleNext() {
Timer { // intensity 40 -> ~4 s between flicks, 100 -> ~1.5 s
interval: 2200 + Math.random() * 3200 var base = 6000 - root.flickerIntensity * 45;
repeat: false interval = Math.max(700, base * (0.6 + Math.random() * 0.8));
running: root.visible && root.digits.length > 0 }
onTriggered: {
flick.stop(); Component.onCompleted: scheduleNext()
flick.start(); }
interval = 2200 + Math.random() * 3200;
restart(); SequentialAnimation {
id: flick
ScriptAction {
script: {
var range = root.flickerIntensity / 100 * 0.75;
glowGroup.opacity = 1 - (0.15 + Math.random() * range);
} }
} }
SequentialAnimation { PauseAnimation { duration: 40 + Math.random() * 140 }
id: flick NumberAnimation {
NumberAnimation { target: glowGroup
target: glowText property: "opacity"
property: "opacity" to: 1.0
to: 0.82 duration: 90 + Math.random() * 160
duration: 70 easing.type: Easing.OutQuad
}
NumberAnimation {
target: glowText
property: "opacity"
to: 1.0
duration: 140
}
} }
} }
} }
} }
// currency indicator, dim amber like an indicator lamp // euro sign in the plain-text font, white
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "\u20AC" text: "\u20AC"
color: "#8a5a20"
font.family: "DejaVu Serif"
font.pixelSize: root.glowFont * 0.6
visible: root.digits.length > 0 visible: root.digits.length > 0
color: "#ffffff"
font.family: Qt.application.font.family
font.pixelSize: root.height * 0.60
} }
// error / empty state // error / empty state
+13
View File
@@ -2,6 +2,7 @@
* Config page for nl.a14r.bitcoinprice. * Config page for nl.a14r.bitcoinprice.
* Follows the canonical Plasma 5 config dialog pattern: * Follows the canonical Plasma 5 config dialog pattern:
* Kirigami.FormLayout + cfg_ prefixed property aliases. * Kirigami.FormLayout + cfg_ prefixed property aliases.
* Mode-specific settings are only visible for the matching style.
*/ */
import QtQuick 2.15 import QtQuick 2.15
@@ -15,6 +16,7 @@ Kirigami.FormLayout {
property alias cfg_displayMode: modeCombo.currentIndex property alias cfg_displayMode: modeCombo.currentIndex
property alias cfg_refreshMinutes: intervalSpin.value property alias cfg_refreshMinutes: intervalSpin.value
property alias cfg_flapSeconds: flapSpin.value property alias cfg_flapSeconds: flapSpin.value
property alias cfg_nixieFlicker: flickSpin.value
Item { Item {
Kirigami.FormData.isSection: true Kirigami.FormData.isSection: true
@@ -28,6 +30,7 @@ Kirigami.FormLayout {
QQC2.SpinBox { QQC2.SpinBox {
id: flapSpin id: flapSpin
visible: modeCombo.currentIndex === 1
Kirigami.FormData.label: i18n("Split-flap cascade (seconds):") Kirigami.FormData.label: i18n("Split-flap cascade (seconds):")
from: 1 from: 1
to: 15 to: 15
@@ -35,6 +38,16 @@ Kirigami.FormLayout {
editable: true 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 { QQC2.SpinBox {
id: intervalSpin id: intervalSpin
Kirigami.FormData.label: i18n("Refresh interval (minutes):") Kirigami.FormData.label: i18n("Refresh interval (minutes):")
+9
View File
@@ -28,6 +28,9 @@ Item {
// Seconds over which the split-flap display cascades to a new price // Seconds over which the split-flap display cascades to a new price
readonly property int flapSeconds: Math.max(1, plasmoid.configuration.flapSeconds) 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 // 0 = plain text, 1 = split-flap board, 2 = nixie tubes
property int displayMode: 0 property int displayMode: 0
readonly property string modeSource: displayMode === 1 ? "DisplayFlip.qml" readonly property string modeSource: displayMode === 1 ? "DisplayFlip.qml"
@@ -143,6 +146,9 @@ Item {
if (item.flapSeconds !== undefined) { if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; }); item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
} }
if (item.flickerIntensity !== undefined) {
item.flickerIntensity = Qt.binding(function () { return root.nixieFlicker; });
}
} }
} }
} }
@@ -181,6 +187,9 @@ Item {
if (item.flapSeconds !== undefined) { if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; }); item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
} }
if (item.flickerIntensity !== undefined) {
item.flickerIntensity = Qt.binding(function () { return root.nixieFlicker; });
}
} }
} }
+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.3.0 X-KDE-PluginInfo-Version=1.4.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