v1.5.0: bigger spaced euro sign, trend arrows on flip/nixie, two-phase flip cascade (reset to 0 over 2s then new values), fix nixie flicker endurance

This commit is contained in:
ArnoldCordewiner
2026-09-05 21:13:33 +02:00
parent b040eb8c47
commit 33dd983463
6 changed files with 126 additions and 69 deletions
+12 -10
View File
@@ -10,19 +10,21 @@ 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 digit flaps, white € sign in the default font, cascading flip animation on price changes (configurable duration), trend LED | | *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 in the default font, trend LED | | *Nixie tubes* | Retro amber glowing digits in glass tubes with configurable random flicker, white € sign and a trend arrow in the default font |
In the split-flap and nixie styles the digits keep their display color and the In the split-flap and nixie styles a trend arrow sits right of the € sign:
trend (up/down/unchanged) is shown on a small LED next to the display. green ▲ when the price went up, red ▼ when it went down. When the price is
Display style changes apply immediately, without restarting the shell. 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.
| Situation | Plain text color | Trend LED | | Situation | Plain text color | Arrow in flip/nixie modes |
|---|---|---| |---|---|---|
| Price up | green | green | | Price up | green | green |
| Price down | red | red | | Price down | red | red |
| Price unchanged / first reading | grey | grey | | Price unchanged | grey | last arrow stays |
| API failure (network error, 4xx/5xx, rate limit 429) | `--` | grey | | First reading / API failure | grey / `--` | no arrow |
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
Binary file not shown.
+68 -39
View File
@@ -1,9 +1,11 @@
/* /*
* Split-flap display mode — airport departure board style. * Split-flap display mode — airport departure board style.
* Only digits are shown as flaps; the euro sign is rendered as plain * Only digits are shown as flaps; the euro sign is rendered as plain
* text (default font, white) next to the board, and the trend shows * text (default font, white) next to the board, followed by a trend
* on a small LED. Changed characters flip with a cascading animation * arrow (green up / red down; unchanged keeps the last arrow).
* spread over the configured number of seconds. * 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. * Pure QtQuick so it can be tested outside the plasmoid shell.
*/ */
@@ -14,8 +16,15 @@ Row {
// wired by the parent loader // wired by the parent loader
property string display: "--" property string display: "--"
property color trendColor: "#9e9e9e" // 1 = up, -1 = down, 0 = unchanged -> keep showing the last arrow
// seconds over which the cascade to a new price is spread 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 property int flapSeconds: 3
spacing: height * 0.06 spacing: height * 0.06
@@ -31,9 +40,9 @@ Row {
return out; return out;
} }
// UI-only derived values (euro visibility); logic must not rely on these — // UI-only derived values (euro/arrow visibility); logic must not rely
// QML gives no ordering guarantee between a change handler and the // on these — QML gives no ordering guarantee between a change handler
// re-evaluation of dependent bindings. // and the re-evaluation of dependent bindings.
readonly property string digitsOnly: digitsOf(display) readonly property string digitsOnly: digitsOf(display)
readonly property string boardText: digitsOnly.length > 0 ? digitsOnly : "--" readonly property string boardText: digitsOnly.length > 0 ? digitsOnly : "--"
@@ -58,21 +67,21 @@ Row {
var digits = digitsOf(display); var digits = digitsOf(display);
var nc = (digits.length > 0 ? digits : "--").split(""); 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
var changed = []; // 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++) { for (var i = 0; i < n; i++) {
var from = i < chars.length ? chars[i] : ""; var cell = rep.itemAt(i);
var to = i < nc.length ? nc[i] : ""; var to = i < nc.length ? nc[i] : "";
if (from !== to) {
changed.push([i, to]);
}
}
var span = Math.max(1, flapSeconds) * 1000 - flapDuration;
var step = changed.length > 1 ? Math.max(0, Math.floor(span / (changed.length - 1))) : 0;
for (var j = 0; j < changed.length; j++) {
var cell = rep.itemAt(changed[j][0]);
if (cell) { if (cell) {
cell.flipTo(changed[j][1], j * step); cell.schedule(to, i * resetStep, valueStart + i * valueStep);
} }
} }
chars = nc; chars = nc;
@@ -92,28 +101,43 @@ Row {
border.color: "#26262b" border.color: "#26262b"
border.width: 1 border.width: 1
// plain property (not a binding) so the flip animation // plain properties (not bindings) so the animations control
// controls when the character actually changes // when the character actually changes
property string ch: "" property string ch: ""
property string pendingCh: "" property string targetCh: ""
property string animCh: ""
Component.onCompleted: { Component.onCompleted: {
ch = root.chars[index] !== undefined ? root.chars[index] : ""; ch = root.chars[index] !== undefined ? root.chars[index] : "";
} }
function flipTo(newCh, delay) { // 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) { if (newCh === ch) {
return; return;
} }
pendingCh = newCh; animCh = newCh;
delayTimer.interval = Math.max(0, delay); flipAnim.restart();
delayTimer.restart();
} }
Timer { Timer {
id: delayTimer id: resetTimer
interval: 0 interval: 0
onTriggered: flipAnim.start() onTriggered: cell.startFlip("0")
}
Timer {
id: valueTimer
interval: 0
onTriggered: cell.startFlip(cell.targetCh)
} }
transform: Scale { transform: Scale {
@@ -132,7 +156,7 @@ Row {
easing.type: Easing.InQuad easing.type: Easing.InQuad
} }
ScriptAction { ScriptAction {
script: cell.ch = cell.pendingCh script: cell.ch = cell.animCh
} }
NumberAnimation { NumberAnimation {
target: scaleTransform target: scaleTransform
@@ -163,6 +187,12 @@ Row {
} }
} }
// 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 // euro sign in the plain-text font, white
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -170,17 +200,16 @@ Row {
visible: root.digitsOnly.length > 0 visible: root.digitsOnly.length > 0
color: "#ffffff" color: "#ffffff"
font.family: Qt.application.font.family font.family: Qt.application.font.family
font.pixelSize: root.height * 0.66 font.pixelSize: root.height * 0.85
} }
// trend LED (green/red/grey) // trend arrow: green up / red down; unchanged keeps the last arrow
Rectangle { Text {
width: root.height * 0.14
height: width
radius: width / 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: root.trendColor text: root.lastDir > 0 ? "\u25B2" : "\u25BC"
border.color: "#26262b" visible: root.digitsOnly.length > 0 && root.lastDir !== 0
border.width: 1 color: root.lastDir > 0 ? "#2ecc71" : "#e74c3c"
font.family: Qt.application.font.family
font.pixelSize: root.height * 0.5
} }
} }
+33 -17
View File
@@ -3,7 +3,8 @@
* Glow is faked with layered text copies, so no QtGraphicalEffects * Glow is faked with layered text copies, so no QtGraphicalEffects
* dependency. Tubes flicker randomly (nixie cathode style); the * dependency. Tubes flicker randomly (nixie cathode style); the
* flicker intensity is configurable (0 = off, 100 = nervous). * flicker intensity is configurable (0 = off, 100 = nervous).
* The euro sign is rendered as plain text (default font, white). * 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. * Pure QtQuick: testable outside the plasmoid shell.
*/ */
@@ -14,7 +15,14 @@ Row {
// wired by the parent loader // wired by the parent loader
property string display: "--" property string display: "--"
property color trendColor: "#9e9e9e" // 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 // 0 = no flicker, 100 = frequent and deep flicker
property int flickerIntensity: 40 property int flickerIntensity: 40
@@ -98,18 +106,21 @@ Row {
} }
} }
// nixie flicker: random dropout of the cathode glow // 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 { Timer {
id: flickTimer id: flickTimer
repeat: false repeat: true
running: root.flickerOn running: root.flickerOn
interval: 1500
onTriggered: { onTriggered: {
flick.restart(); flick.restart();
scheduleNext(); scheduleNext();
} }
function scheduleNext() { function scheduleNext() {
// intensity 40 -> ~4 s between flicks, 100 -> ~1.5 s
var base = 6000 - root.flickerIntensity * 45; var base = 6000 - root.flickerIntensity * 45;
interval = Math.max(700, base * (0.6 + Math.random() * 0.8)); interval = Math.max(700, base * (0.6 + Math.random() * 0.8));
} }
@@ -137,6 +148,12 @@ Row {
} }
} }
// 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 // euro sign in the plain-text font, white
Text { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -144,7 +161,17 @@ Row {
visible: root.digits.length > 0 visible: root.digits.length > 0
color: "#ffffff" color: "#ffffff"
font.family: Qt.application.font.family font.family: Qt.application.font.family
font.pixelSize: root.height * 0.60 font.pixelSize: root.height * 0.85
}
// 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.45
} }
// error / empty state // error / empty state
@@ -156,15 +183,4 @@ Row {
font.pixelSize: root.glowFont * 0.8 font.pixelSize: root.glowFont * 0.8
visible: root.digits.length === 0 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
}
} }
+10
View File
@@ -142,7 +142,12 @@ Item {
source: root.modeSource source: root.modeSource
onLoaded: { onLoaded: {
item.display = Qt.binding(function () { return root.displayText; }); item.display = Qt.binding(function () { return root.displayText; });
if (item.trendColor !== undefined) {
item.trendColor = Qt.binding(function () { return root.priceColor; }); item.trendColor = Qt.binding(function () { return root.priceColor; });
}
if (item.trendDir !== undefined) {
item.trendDir = Qt.binding(function () { return root.trend; });
}
if (item.flapSeconds !== undefined) { if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; }); item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
} }
@@ -183,7 +188,12 @@ Item {
source: root.modeSource source: root.modeSource
onLoaded: { onLoaded: {
item.display = Qt.binding(function () { return root.displayText; }); item.display = Qt.binding(function () { return root.displayText; });
if (item.trendColor !== undefined) {
item.trendColor = Qt.binding(function () { return root.priceColor; }); item.trendColor = Qt.binding(function () { return root.priceColor; });
}
if (item.trendDir !== undefined) {
item.trendDir = Qt.binding(function () { return root.trend; });
}
if (item.flapSeconds !== undefined) { if (item.flapSeconds !== undefined) {
item.flapSeconds = Qt.binding(function () { return root.flapSeconds; }); item.flapSeconds = Qt.binding(function () { return root.flapSeconds; });
} }
+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.4.0 X-KDE-PluginInfo-Version=1.5.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