diff --git a/README.md b/README.md index a52e8c5..9ecbf1c 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,21 @@ Three display styles, configurable in the settings dialog: | Style | Description | |---|---| | *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 | -| *Nixie tubes* | Retro amber glowing digits in glass tubes with configurable random flicker, white € sign in the default font, 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 and a trend arrow in the default font | -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. -Display style changes apply immediately, without restarting the shell. +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. -| Situation | Plain text color | Trend LED | +| Situation | Plain text color | Arrow in flip/nixie modes | |---|---|---| -| Price up | green | green | -| Price down | red | red | -| Price unchanged / first reading | grey | grey | -| API failure (network error, 4xx/5xx, rate limit 429) | `--` | grey | +| Price up | green | green ▲ | +| Price down | red | red ▼ | +| Price unchanged | grey | last arrow stays | +| First reading / API failure | grey / `--` | no arrow | Format: `68,969 €` (thousands separators follow the system locale, rounded to whole euros) preceded by the bundled Bitcoin icon in the compact and full diff --git a/bitcoin-price-1.5.0.plasmoid b/bitcoin-price-1.5.0.plasmoid new file mode 100644 index 0000000..94db64a Binary files /dev/null and b/bitcoin-price-1.5.0.plasmoid differ diff --git a/contents/ui/DisplayFlip.qml b/contents/ui/DisplayFlip.qml index 01599a9..da15c88 100644 --- a/contents/ui/DisplayFlip.qml +++ b/contents/ui/DisplayFlip.qml @@ -1,9 +1,11 @@ /* * 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, and the trend shows - * on a small LED. Changed characters flip with a cascading animation - * spread over the configured number of seconds. + * 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. */ @@ -14,8 +16,15 @@ Row { // wired by the parent loader property string display: "--" - property color trendColor: "#9e9e9e" - // seconds over which the cascade to a new price is spread + // 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 @@ -31,9 +40,9 @@ Row { 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. + // 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 : "--" @@ -58,21 +67,21 @@ Row { var digits = digitsOf(display); var nc = (digits.length > 0 ? digits : "--").split(""); 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++) { - var from = i < chars.length ? chars[i] : ""; + var cell = rep.itemAt(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) { - cell.flipTo(changed[j][1], j * step); + cell.schedule(to, i * resetStep, valueStart + i * valueStep); } } chars = nc; @@ -92,28 +101,43 @@ Row { border.color: "#26262b" border.width: 1 - // plain property (not a binding) so the flip animation - // controls when the character actually changes + // plain properties (not bindings) so the animations control + // when the character actually changes property string ch: "" - property string pendingCh: "" + property string targetCh: "" + property string animCh: "" Component.onCompleted: { 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) { return; } - pendingCh = newCh; - delayTimer.interval = Math.max(0, delay); - delayTimer.restart(); + animCh = newCh; + flipAnim.restart(); } Timer { - id: delayTimer + id: resetTimer interval: 0 - onTriggered: flipAnim.start() + onTriggered: cell.startFlip("0") + } + + Timer { + id: valueTimer + interval: 0 + onTriggered: cell.startFlip(cell.targetCh) } transform: Scale { @@ -132,7 +156,7 @@ Row { easing.type: Easing.InQuad } ScriptAction { - script: cell.ch = cell.pendingCh + script: cell.ch = cell.animCh } NumberAnimation { 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 Text { anchors.verticalCenter: parent.verticalCenter @@ -170,17 +200,16 @@ Row { visible: root.digitsOnly.length > 0 color: "#ffffff" font.family: Qt.application.font.family - font.pixelSize: root.height * 0.66 + font.pixelSize: root.height * 0.85 } - // trend LED (green/red/grey) - Rectangle { - width: root.height * 0.14 - height: width - radius: width / 2 + // trend arrow: green up / red down; unchanged keeps the last arrow + Text { anchors.verticalCenter: parent.verticalCenter - color: root.trendColor - border.color: "#26262b" - border.width: 1 + 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.5 } } diff --git a/contents/ui/DisplayNixie.qml b/contents/ui/DisplayNixie.qml index fd7c112..328cc79 100644 --- a/contents/ui/DisplayNixie.qml +++ b/contents/ui/DisplayNixie.qml @@ -3,7 +3,8 @@ * 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 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. */ @@ -14,7 +15,14 @@ Row { // wired by the parent loader 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 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 { id: flickTimer - repeat: false + repeat: true running: root.flickerOn + interval: 1500 onTriggered: { flick.restart(); scheduleNext(); } function scheduleNext() { - // intensity 40 -> ~4 s between flicks, 100 -> ~1.5 s var base = 6000 - root.flickerIntensity * 45; 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 Text { anchors.verticalCenter: parent.verticalCenter @@ -144,7 +161,17 @@ Row { visible: root.digits.length > 0 color: "#ffffff" 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 @@ -156,15 +183,4 @@ Row { 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 - } } diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 6a51e46..99d0c62 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -142,7 +142,12 @@ Item { source: root.modeSource onLoaded: { item.display = Qt.binding(function () { return root.displayText; }); - item.trendColor = Qt.binding(function () { return root.priceColor; }); + 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; }); } @@ -183,7 +188,12 @@ Item { source: root.modeSource onLoaded: { item.display = Qt.binding(function () { return root.displayText; }); - item.trendColor = Qt.binding(function () { return root.priceColor; }); + 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; }); } diff --git a/metadata.desktop b/metadata.desktop index 99193d1..4ec00b8 100644 --- a/metadata.desktop +++ b/metadata.desktop @@ -10,7 +10,7 @@ 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.4.0 +X-KDE-PluginInfo-Version=1.5.0 X-KDE-PluginInfo-Website=https://gitea.a14r.be/hermes/plasma-bitcoin-price X-KDE-PluginInfo-Category=Utilities X-KDE-PluginInfo-License=MIT