Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e9b94217e | ||
|
|
e4e848540c | ||
|
|
b11fe06276 | ||
|
|
33dd983463 |
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+81
-42
@@ -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,24 +187,39 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// euro sign in the plain-text font, white
|
||||
// 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.verticalCenter: parent.verticalCenter
|
||||
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.66
|
||||
font.pixelSize: root.height * 0.85
|
||||
}
|
||||
|
||||
// trend LED (green/red/grey)
|
||||
Rectangle {
|
||||
width: root.height * 0.14
|
||||
height: width
|
||||
radius: width / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.trendColor
|
||||
border.color: "#26262b"
|
||||
border.width: 1
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +148,40 @@ Row {
|
||||
}
|
||||
}
|
||||
|
||||
// euro sign in the plain-text font, white
|
||||
// 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.verticalCenter: parent.verticalCenter
|
||||
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.60
|
||||
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
|
||||
@@ -156,15 +193,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
|
||||
}
|
||||
}
|
||||
|
||||
+16
-6
@@ -127,8 +127,8 @@ Item {
|
||||
Image {
|
||||
id: btcIcon
|
||||
source: "../images/bitcoin.svg"
|
||||
sourceSize.width: PlasmaCore.Units.iconSizes.smallMedium
|
||||
sourceSize.height: PlasmaCore.Units.iconSizes.smallMedium
|
||||
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"
|
||||
@@ -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; });
|
||||
}
|
||||
@@ -169,8 +174,8 @@ Item {
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
source: "../images/bitcoin.svg"
|
||||
sourceSize.width: PlasmaCore.Units.iconSizes.large
|
||||
sourceSize.height: PlasmaCore.Units.iconSizes.large
|
||||
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"
|
||||
}
|
||||
@@ -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; });
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.6.0
|
||||
X-KDE-PluginInfo-Website=https://gitea.a14r.be/hermes/plasma-bitcoin-price
|
||||
X-KDE-PluginInfo-Category=Utilities
|
||||
X-KDE-PluginInfo-License=MIT
|
||||
|
||||
Reference in New Issue
Block a user