diff --git a/README.md b/README.md index 262866a..a6075eb 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,25 @@ 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). -## Display +## Display styles -| Situation | Display | +Three display styles, configurable in the settings dialog: + +| Style | Description | |---|---| -| Price up compared to the previous reading | green | -| Price down compared to the previous reading | red | -| Price unchanged (or first reading) | grey | -| API failure (network error, 4xx/5xx, rate limit 429) | `--` | +| *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 | +| *Nixie tubes* | Retro amber glowing digits in glass tubes (with a soft flicker), trend LED | + +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 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 ### 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 to the [v1.1.0 release](../../releases/tag/v1.1.0). ### Terminal ```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: plasmashell --replace & ``` diff --git a/bitcoin-price-1.2.0.plasmoid b/bitcoin-price-1.2.0.plasmoid new file mode 100644 index 0000000..4024231 Binary files /dev/null and b/bitcoin-price-1.2.0.plasmoid differ diff --git a/contents/config/main.xml b/contents/config/main.xml index cc8de78..2aff24a 100644 --- a/contents/config/main.xml +++ b/contents/config/main.xml @@ -5,6 +5,15 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd"> + + + + + + + + 0 + 5 diff --git a/contents/ui/DisplayFlip.qml b/contents/ui/DisplayFlip.qml new file mode 100644 index 0000000..f041aae --- /dev/null +++ b/contents/ui/DisplayFlip.qml @@ -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 + } +} diff --git a/contents/ui/DisplayNixie.qml b/contents/ui/DisplayNixie.qml new file mode 100644 index 0000000..b0a1481 --- /dev/null +++ b/contents/ui/DisplayNixie.qml @@ -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 + } +} diff --git a/contents/ui/DisplayText.qml b/contents/ui/DisplayText.qml new file mode 100644 index 0000000..628f498 --- /dev/null +++ b/contents/ui/DisplayText.qml @@ -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 +} diff --git a/contents/ui/configGeneral.qml b/contents/ui/configGeneral.qml index bfa6cee..b57c85d 100644 --- a/contents/ui/configGeneral.qml +++ b/contents/ui/configGeneral.qml @@ -12,12 +12,19 @@ Kirigami.FormLayout { id: generalPage // The cfg_ prefix is what Plasma uses to read/write plasmoid.configuration + property alias cfg_displayMode: modeCombo.currentIndex property alias cfg_refreshMinutes: intervalSpin.value Item { 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 { id: intervalSpin Kirigami.FormData.label: i18n("Refresh interval (minutes):") diff --git a/contents/ui/main.qml b/contents/ui/main.qml index c0da2a3..d684e0d 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -7,6 +7,10 @@ * - Grey : unchanged (or first reading) * - "--" : 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 * (CoinGecko limit: 1 call per 5 minutes). */ @@ -20,6 +24,12 @@ Item { id: root 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" // -1 = down, 0 = unchanged / no previous reading yet, 1 = up @@ -61,8 +71,13 @@ Item { } Component.onCompleted: { - console.log("[bitcoinprice] started, refresh interval:", - refreshMinutes, "minutes"); + try { + 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 @@ -75,7 +90,9 @@ Item { Item { 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 Row { @@ -93,19 +110,16 @@ Item { visible: root.state === "ok" } - Text { - id: priceLabel + Loader { + id: displayLoader anchors.verticalCenter: parent.verticalCenter - text: root.displayText - color: root.priceColor - textFormat: Text.PlainText - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.family: PlasmaCore.Theme.defaultFont.family - font.pixelSize: compact.height > PlasmaCore.Units.iconSizes.small - ? Math.round(compact.height * 0.6) - : PlasmaCore.Theme.defaultFont.pixelSize + height: compact.height * 0.62 + width: item ? item.implicitWidth : 0 + source: root.modeSource + onLoaded: { + item.display = Qt.binding(function () { return root.displayText; }); + item.trendColor = Qt.binding(function () { return root.priceColor; }); + } } } } @@ -115,8 +129,8 @@ Item { Item { id: fullRep - implicitWidth: PlasmaCore.Units.gridUnit * 14 - implicitHeight: PlasmaCore.Units.gridUnit * 9 + implicitWidth: PlasmaCore.Units.gridUnit * 16 + implicitHeight: PlasmaCore.Units.gridUnit * 10 Column { anchors.centerIn: parent @@ -131,12 +145,16 @@ Item { visible: root.state === "ok" } - Text { + Loader { + id: fullDisplayLoader anchors.horizontalCenter: parent.horizontalCenter - text: root.displayText - color: root.priceColor - font.family: PlasmaCore.Theme.defaultFont.family - font.pixelSize: PlasmaCore.Units.gridUnit * 2 + height: PlasmaCore.Units.gridUnit * 2.4 + width: item ? item.implicitWidth : 0 + source: root.modeSource + onLoaded: { + item.display = Qt.binding(function () { return root.displayText; }); + item.trendColor = Qt.binding(function () { return root.priceColor; }); + } } Text { diff --git a/metadata.desktop b/metadata.desktop index 02dd7a0..d0e3d90 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.1.1 +X-KDE-PluginInfo-Version=1.2.0 X-KDE-PluginInfo-Website=https://gitea.a14r.be/hermes/plasma-bitcoin-price X-KDE-PluginInfo-Category=Utilities X-KDE-PluginInfo-License=MIT