Files
quickdots/bar/modules/Crypto.qml
2026-05-17 22:30:27 +00:00

1090 lines
55 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "../../config" as Cfg
import "../../components"
ModuleChip {
id: root
readonly property var cfg: Cfg.Config
ConfigWriter {
id: configWriter
}
width: chipRow.implicitWidth + 24
radius: panelRadius
chipColor: t.cryptoBg
chipColorHovered: t.cryptoBgHover
chipBorderColor: t.cryptoBorder
chipBorderColorHovered: t.cryptoBorderHover
chipHoverEnabled: true
chipTapAction: () => cryptoPopup.toggle()
popupOpen: cryptoPopup.popupOpen
chipAnimWidth: true
property var localPairs: [{ coin: "bitcoin", currency: "usd" }]
property int activeCoinIndex: 0
readonly property string localCoinId: localPairs.length > 0
? localPairs[activeCoinIndex % localPairs.length].coin
: "bitcoin"
readonly property string localActiveCurrency: localPairs.length > 0
? localPairs[activeCoinIndex % localPairs.length].currency
: "usd"
property string localVsCur: "usd"
property int localDecimals: cfg.cryptoDecimals
property int localRefreshSec: cfg.cryptoRefreshSec
property bool localShowIcon: cfg.cryptoShowIcon
function resolveWithPersistence() {
root.localPairs = Array.isArray(cfg.cryptoPairs) && cfg.cryptoPairs.length > 0 ? cfg.cryptoPairs : [{ coin: "bitcoin", currency: "usd" }]
root.localVsCur = root.localPairs[0].currency
root.displayCurrency = root.localPairs[0].currency
root.activeCoinIndex = 0
root.localDecimals = cfg.cryptoDecimals
root.localRefreshSec = cfg.cryptoRefreshSec
root.localShowIcon = cfg.cryptoShowIcon
}
function persistSettings() {
const pairs = root.localPairs
const decimals = Math.round(root.localDecimals)
const refreshSec = Math.round(root.localRefreshSec)
const showIcon = root.localShowIcon
const pairsQml = "[" + pairs.map(p =>
'{ coin: "' + p.coin + '", currency: "' + p.currency + '" }'
).join(", ") + "]"
configWriter.writeInts([
{ key: "cryptoDecimals", value: decimals },
{ key: "cryptoRefreshSec", value: refreshSec }
])
configWriter.writeBool("cryptoShowIcon", showIcon)
configWriter.writeArray("cryptoPairs", pairsQml)
}
property real price: -1
property real change24h: 0
property real high24h: 0
property real low24h: 0
property real marketCap: 0
property real volume24h: 0
property int rank: 0
property real circulatingSupply: 0
property real ath: 0
property real athChange: 0
property string coinSymbol: localCoinId.toUpperCase().slice(0, 4)
property string coinName: ""
property string lastUpdated: ""
property string displayCurrency: "usd"
property bool loading: false
property bool loadingVisible: false
property bool hasError: false
Timer {
id: minLoadTimer
interval: 2000
repeat: false
onTriggered: { if (!root.loading) root.loadingVisible = false }
}
onLoadingChanged: {
if (loading) {
loadingVisible = true
minLoadTimer.restart()
} else {
if (!minLoadTimer.running)
loadingVisible = false
}
}
function compactPrice(val) {
if (val < 0) return "—"
const factor = Math.pow(10, localDecimals)
if (val >= 1_000_000) {
const n = Math.trunc(val * factor / 1_000_000)
return (n / factor).toFixed(localDecimals) + "m"
}
if (val >= 1_000) {
const n = Math.trunc(val * factor / 1_000)
return (n / factor).toFixed(localDecimals) + "k"
}
return val.toFixed(Math.max(localDecimals, 2))
}
function fullPrice(val) {
if (val < 0) return "—"
return val.toLocaleString(Qt.locale(), "f", 2)
}
function compactLarge(val) {
if (val <= 0) return "—"
if (val >= 1_000_000_000) return (val / 1_000_000_000).toFixed(2) + "B"
if (val >= 1_000_000) return (val / 1_000_000).toFixed(2) + "M"
if (val >= 1_000) return (val / 1_000).toFixed(2) + "k"
return val.toFixed(2)
}
function safeNumber(val, fallback) {
const n = Number(val)
return (typeof n === "number" && isFinite(n)) ? n : fallback
}
function safeString(val, maxLen, fallback) {
if (typeof val !== "string") return fallback
return val.slice(0, maxLen).replace(/[\x00-\x1f\x7f-\x9f]/g, "")
}
function fetchPrice() {
if (loading) return
loading = true
hasError = false
const url =
"https://api.coingecko.com/api/v3/coins/markets" +
"?vs_currency=" + encodeURIComponent(localActiveCurrency || "usd") +
"&ids=" + encodeURIComponent(localCoinId || "bitcoin") +
"&order=market_cap_desc&per_page=1&page=1" +
"&sparkline=false&price_change_percentage=24h"
const xhr = new XMLHttpRequest()
xhr.timeout = 15000
xhr.onreadystatechange = function () {
if (xhr.readyState !== XMLHttpRequest.DONE) return
loading = false
if (xhr.status !== 200) { hasError = true; return }
try {
const text = xhr.responseText
if (text.length > 65536) { hasError = true; return }
const data = JSON.parse(text)
if (!Array.isArray(data) || data.length === 0) { hasError = true; return }
const coin = data[0]
if (typeof coin !== "object" || coin === null) { hasError = true; return }
price = safeNumber(coin.current_price, -1)
change24h = safeNumber(coin.price_change_percentage_24h, 0)
high24h = safeNumber(coin.high_24h, 0)
low24h = safeNumber(coin.low_24h, 0)
marketCap = safeNumber(coin.market_cap, 0)
volume24h = safeNumber(coin.total_volume, 0)
rank = Math.trunc(safeNumber(coin.market_cap_rank, 0))
circulatingSupply = safeNumber(coin.circulating_supply, 0)
ath = safeNumber(coin.ath, 0)
athChange = safeNumber(coin.ath_change_percentage, 0)
coinSymbol = safeString(coin.symbol, 8, localCoinId).toUpperCase()
coinName = safeString(coin.name, 50, "")
lastUpdated = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
root.displayCurrency = root.localActiveCurrency
root.activeCoinIndex = (root.activeCoinIndex + 1) % Math.max(1, root.localPairs.length)
} catch (_) {
hasError = true
}
}
xhr.open("GET", url)
xhr.setRequestHeader("Accept", "application/json")
xhr.send()
}
Component.onCompleted: { resolveWithPersistence(); fetchPrice() }
Timer {
id: refreshTimer
interval: root.localRefreshSec * 1000
running: true
repeat: true
onTriggered: root.fetchPrice()
}
onLocalRefreshSecChanged: refreshTimer.restart()
RowLayout {
id: chipRow
anchors.centerIn: parent
spacing: 0
SequentialAnimation on opacity {
running: root.loadingVisible
loops: Animation.Infinite
NumberAnimation { from: 1; to: 0.25; duration: 500; easing.type: Easing.InOutSine }
NumberAnimation { from: 0.25; to: 1; duration: 500; easing.type: Easing.InOutSine }
}
Connections {
target: root
function onLoadingVisibleChanged() {
if (!root.loadingVisible) chipRow.opacity = 1
}
}
Text {
visible: root.localShowIcon
text: "󰿤"
color: t.cryptoIcon
font.pixelSize: 14
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 7
}
Text {
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
color: t.cryptoLabel
font.pixelSize: 12
verticalAlignment: Text.AlignVCenter
}
Text {
text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
color: t.cryptoValue
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
}
Text {
visible: root.price >= 0
text: root.change24h >= 0 ? "▲" : "▼"
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
font.pixelSize: 9
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 4
Behavior on color { ColorAnimation { duration: 200 } }
}
}
StandardPopup {
id: cryptoPopup
ns: "crypto"
chipRoot: root
cardWidth: 300
hasKeyboardFocus: true
cardColor: t.cryptoPopupBg
cardBorderColor: t.cryptoPopupBorder
cardHeight: cardCol.implicitHeight + 24
layerEnabled: true
ColumnLayout {
id: cardCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 16
anchors.topMargin: 16
spacing: 10
Rectangle {
Layout.fillWidth: true
height: 30
radius: 10
color: t.cryptoFieldBg
RowLayout {
anchors.fill: parent
anchors.margins: 3
spacing: 3
component TabBtn: Rectangle {
required property string label
required property int tabIndex
Layout.fillWidth: true
height: 24
radius: 8
readonly property bool active: cryptoPopup.activeTab === tabIndex
color: active
? (t.accent)
: (tm.containsMouse ? (t.cryptoFieldBgFocus) : "transparent")
Behavior on color { ColorAnimation { duration: 130 } }
Text {
anchors.centerIn: parent
text: parent.label
color: parent.active ? (t.cryptoTextOnAccent) : (t.cryptoPopupDim)
font.pixelSize: 11
font.bold: parent.active
Behavior on color { ColorAnimation { duration: 130 } }
}
MouseArea {
id: tm
anchors.fill: parent; hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: cryptoPopup.activeTab = tabIndex
}
}
TabBtn { label: "Price"; tabIndex: 0 }
TabBtn { label: "Settings"; tabIndex: 1 }
}
}
Item {
id: tabContainer
Layout.fillWidth: true
clip: true
Layout.preferredHeight: Math.max(priceContent.implicitHeight,
settingsContent.implicitHeight)
ColumnLayout {
id: priceContent
width: tabContainer.width
x: cryptoPopup.activeTab === 0 ? 0 : -tabContainer.width
opacity: cryptoPopup.activeTab === 0 ? 1.0 : 0.0
Behavior on x { NumberAnimation { duration: 260; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
spacing: 10
RowLayout {
Layout.fillWidth: true
Text {
text: "󰿤"
color: t.cryptoIcon
font.pixelSize: 22
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
spacing: 1
Layout.fillWidth: true
Layout.leftMargin: 8
Text {
text: root.coinSymbol + " / " + root.displayCurrency.toUpperCase()
color: t.cryptoPopupHeader
font.pixelSize: 15
font.bold: true
}
Text {
text: root.coinName !== "" ? root.coinName : (root.localCoinId.charAt(0).toUpperCase() + root.localCoinId.slice(1))
color: t.cryptoPopupDim
font.pixelSize: 11
}
}
Rectangle {
width: badge.implicitWidth + 12
height: 22; radius: 11
color: root.change24h >= 0 ? (t.cryptoUpBg) : (t.cryptoDownBg)
Text {
id: badge
anchors.centerIn: parent
text: (root.change24h >= 0 ? "▲ +" : "▼ ") + root.change24h.toFixed(2) + "%"
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
font.pixelSize: 11
font.bold: true
}
}
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: root.price < 0 ? "Loading…" : root.displayCurrency.toUpperCase() + " " + root.fullPrice(root.price)
color: t.cryptoPopupText
font.pixelSize: 24
font.bold: true
font.family: "monospace"
}
Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator }
component StatRow: RowLayout {
property string label: ""
property string value: ""
Layout.fillWidth: true
Text { text: label; color: t.cryptoPopupDim; font.pixelSize: 12; Layout.fillWidth: true }
Text { text: value; color: t.cryptoPopupText; font.pixelSize: 12; font.bold: true; font.family: "monospace" }
}
StatRow { label: "Rank"; value: root.rank > 0 ? "#" + root.rank : "—" }
StatRow { label: "24h High"; value: root.high24h > 0 ? root.fullPrice(root.high24h) : "—" }
StatRow { label: "24h Low"; value: root.low24h > 0 ? root.fullPrice(root.low24h) : "—" }
StatRow { label: "Market Cap"; value: root.compactLarge(root.marketCap) }
StatRow { label: "24h Volume"; value: root.compactLarge(root.volume24h) }
StatRow { label: "Circ. Supply"; value: root.circulatingSupply > 0 ? root.compactLarge(root.circulatingSupply) + " " + root.coinSymbol : "—" }
RowLayout {
Layout.fillWidth: true
Text { text: "ATH"; color: t.cryptoPopupDim; font.pixelSize: 12; Layout.fillWidth: true }
Text {
text: root.ath > 0 ? root.fullPrice(root.ath) : "—"
color: t.cryptoPopupText
font.pixelSize: 12; font.bold: true; font.family: "monospace"
}
Text {
visible: root.ath > 0
text: "(" + root.athChange.toFixed(2) + "%)"
color: t.cryptoDown
font.pixelSize: 11; font.bold: true; font.family: "monospace"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
RowLayout {
Layout.fillWidth: true
Text {
text: "24h Range"
color: t.cryptoPopupDim
font.pixelSize: 12
Layout.fillWidth: true
}
Text {
visible: root.high24h > root.low24h && root.price >= 0
text: ((root.price - root.low24h) / (root.high24h - root.low24h) * 100).toFixed(1) + "% of range"
color: t.cryptoPopupDim
font.pixelSize: 11
}
}
Item {
Layout.fillWidth: true
height: 6
Rectangle {
anchors.fill: parent
radius: 3
color: t.cryptoRangeTrack
}
Rectangle {
height: parent.height
radius: 3
color: root.change24h >= 0
? (t.cryptoUp)
: (t.cryptoDown)
width: (root.high24h > root.low24h && root.price >= 0)
? Math.max(radius * 2,
Math.min(parent.width,
(root.price - root.low24h) / (root.high24h - root.low24h)
* parent.width))
: 0
Behavior on width { NumberAnimation { duration: 500; easing.type: Easing.OutCubic } }
}
}
RowLayout {
Layout.fillWidth: true
Text {
text: root.low24h > 0 ? root.fullPrice(root.low24h) : "—"
color: t.cryptoDown
font.pixelSize: 10
font.family: "monospace"
}
Item { Layout.fillWidth: true }
Text {
text: root.high24h > 0 ? root.fullPrice(root.high24h) : "—"
color: t.cryptoUp
font.pixelSize: 10
font.family: "monospace"
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator }
RowLayout {
Layout.fillWidth: true
Text {
text: root.hasError ? "⚠ fetch error" : "Updated " + (root.lastUpdated || "—")
color: t.cryptoPopupDim
font.pixelSize: 11
Layout.fillWidth: true
}
Rectangle {
id: refreshBtn
width: 26; height: 26; radius: 8
color: rMouse.containsMouse
? (rMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
: "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
Text {
id: refreshIcon
anchors.centerIn: parent
text: "󰑐"
color: t.cryptoIcon
font.pixelSize: 14
RotationAnimator {
id: spinAnim
target: refreshIcon
from: 0; to: 360
duration: 600
easing.type: Easing.OutCubic
running: false
}
}
MouseArea {
id: rMouse
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: {
spinAnim.restart()
root.fetchPrice()
}
}
}
}
}
ColumnLayout {
id: settingsContent
width: tabContainer.width
x: cryptoPopup.activeTab === 1 ? 0 : tabContainer.width
opacity: cryptoPopup.activeTab === 1 ? 1.0 : 0.0
Behavior on x { NumberAnimation { duration: 260; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
spacing: 12
component FieldRow: ColumnLayout {
property string label: ""
property string placeholder: ""
property alias fieldText: fi.text
spacing: 4
Layout.fillWidth: true
Text {
text: label
color: t.cryptoPopupDim
font.pixelSize: 11
font.capitalization: Font.AllUppercase
}
Rectangle {
Layout.fillWidth: true
height: 30
radius: 8
color: fi.activeFocus ? (t.cryptoFieldBgFocus) : (t.cryptoFieldBg)
border.color: fi.activeFocus ? (t.accent) : "transparent"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
TextInput {
id: fi
anchors { fill: parent; leftMargin: 10; rightMargin: 10; topMargin: 7; bottomMargin: 7 }
color: t.cryptoPopupText
font.pixelSize: 12
font.family: "monospace"
selectionColor: t.accent
clip: true
Text {
anchors.fill: parent
text: parent.parent.parent.placeholder
color: t.cryptoPlaceholder
font.pixelSize: 12
font.family: "monospace"
visible: fi.text === "" && !fi.activeFocus
verticalAlignment: Text.AlignVCenter
}
}
}
}
component StepRow: RowLayout {
property string label: ""
property int value: 1
property int minVal: 0
property int maxVal: 999
signal stepped(int v)
Layout.fillWidth: true
Text { text: label; color: t.cryptoPopupDim; font.pixelSize: 12; Layout.fillWidth: true }
Rectangle {
width: 24; height: 24; radius: 7
color: mm.containsMouse ? (mm.pressed ? (t.accent) : (t.cryptoFieldBgFocus)) : (t.cryptoFieldBg)
Behavior on color { ColorAnimation { duration: 100 } }
Text { anchors.centerIn: parent; text: ""; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText }
MouseArea { id: mm; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: if (value > minVal) stepped(value - 1) }
}
Rectangle {
width: 54; height: 24; radius: 6; color: t.cryptoFieldBg
Text { anchors.centerIn: parent; text: value; color: t.cryptoPopupText; font.pixelSize: 12; font.bold: true; font.family: "monospace" }
}
Rectangle {
width: 24; height: 24; radius: 7
color: pm.containsMouse ? (pm.pressed ? (t.accent) : (t.cryptoFieldBgFocus)) : (t.cryptoFieldBg)
Behavior on color { ColorAnimation { duration: 100 } }
Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText }
MouseArea { id: pm; anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: if (value < maxVal) stepped(value + 1) }
}
}
component ToggleRow: RowLayout {
property string label: ""
property bool checked: false
signal toggled(bool v)
Layout.fillWidth: true
Text { text: label; color: t.cryptoPopupDim; font.pixelSize: 12; Layout.fillWidth: true }
Rectangle {
width: 36; height: 20; radius: 10
color: checked ? (t.accent) : (t.cryptoFieldBgFocus)
Behavior on color { ColorAnimation { duration: 150 } }
Rectangle {
width: 14; height: 14; radius: 7; color: "#ffffffff"
anchors.verticalCenter: parent.verticalCenter
x: checked ? parent.width - 17 : 3
Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
}
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: toggled(!checked) }
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: "Pairs to cycle"
color: t.cryptoPopupDim
font.pixelSize: 11
font.capitalization: Font.AllUppercase
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Item {
Layout.fillWidth: true
height: 24
clip: true
Rectangle {
anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
width: 12; z: 2
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: t.cryptoPopupBg }
GradientStop { position: 1.0; color: "transparent" }
}
visible: chipScroll.contentX > 0
}
Rectangle {
anchors { right: parent.right; top: parent.top; bottom: parent.bottom }
width: 12; z: 2
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: t.cryptoPopupBg }
}
visible: chipScroll.contentX < chipScroll.contentWidth - chipScroll.width - 1
}
Flickable {
id: chipScroll
anchors.fill: parent
contentWidth: chipRow2.implicitWidth
contentHeight: height
clip: true
flickableDirection: Flickable.HorizontalFlick
boundsBehavior: Flickable.StopAtBounds
Row {
id: chipRow2
spacing: 8
Repeater {
model: root.localPairs
delegate: Item {
readonly property bool isActive:
index === root.activeCoinIndex % Math.max(1, root.localPairs.length)
height: 24
width: chipLabel.implicitWidth + dotSpace.width + 6
+ (root.localPairs.length > 1 ? 18 : 0)
Rectangle {
id: dotSpace
anchors.verticalCenter: parent.verticalCenter
x: 0; width: 5; height: 5; radius: 3
color: isActive ? (t.accent) : "#33ffffff"
Behavior on color { ColorAnimation { duration: 150 } }
}
Text {
id: chipLabel
anchors.verticalCenter: parent.verticalCenter
x: dotSpace.width + 5
text: (modelData.coin ?? "?") + "/" + (modelData.currency ?? "?").toUpperCase()
color: isActive
? (t.accent)
: (t.cryptoPopupText)
font.pixelSize: 12
font.bold: isActive
font.family: "monospace"
Behavior on color { ColorAnimation { duration: 150 } }
}
Rectangle {
visible: root.localPairs.length > 1
anchors { right: parent.right
verticalCenter: parent.verticalCenter }
width: 14; height: 14; radius: 4
color: rmChipMouse.containsMouse ? (t.cryptoChipRemoveBg) : "transparent"
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent
text: "×"; font.pixelSize: 11; font.bold: true
color: t.cryptoDown
}
MouseArea {
id: rmChipMouse
anchors.fill: parent; hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let arr = root.localPairs.slice()
arr.splice(index, 1)
root.localPairs = arr
if (root.activeCoinIndex >= arr.length)
root.activeCoinIndex = 0
}
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
height: 3
visible: chipScroll.contentWidth > chipScroll.width
Rectangle {
anchors.fill: parent
radius: 2
color: t.cryptoFieldBg
}
Rectangle {
height: parent.height
radius: 2
color: t.accent
opacity: 0.7
readonly property real ratio: chipScroll.width / Math.max(1, chipScroll.contentWidth)
width: Math.max(16, parent.width * ratio)
x: (parent.width - width) *
(chipScroll.contentWidth > chipScroll.width
? chipScroll.contentX / (chipScroll.contentWidth - chipScroll.width)
: 0)
Behavior on x { NumberAnimation { duration: 80 } }
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: "Coin ID"
color: t.cryptoPopupDim
font.pixelSize: 11
font.capitalization: Font.AllUppercase
}
Rectangle {
Layout.fillWidth: true
height: 30; radius: 8
color: addCoinInput.activeFocus ? (t.cryptoFieldBgFocus) : (t.cryptoFieldBg)
border.color: addCoinInput.activeFocus ? (t.accent) : "transparent"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
TextInput {
id: addCoinInput
anchors { fill: parent; leftMargin: 10; rightMargin: 10; topMargin: 7; bottomMargin: 7 }
color: t.cryptoPopupText
font.pixelSize: 12; font.family: "monospace"
selectionColor: t.accent
clip: true
Text {
anchors.fill: parent
text: "e.g. bitcoin, monero, solana"
color: t.cryptoPlaceholder
font.pixelSize: 12; font.family: "monospace"
visible: parent.text === "" && !parent.activeFocus
verticalAlignment: Text.AlignVCenter
}
onAccepted: addCurrencyInput.forceActiveFocus()
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: "Currency"
color: t.cryptoPopupDim
font.pixelSize: 11
font.capitalization: Font.AllUppercase
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Rectangle {
Layout.fillWidth: true
height: 30; radius: 8
color: addCurrencyInput.activeFocus ? (t.cryptoFieldBgFocus) : (t.cryptoFieldBg)
border.color: addCurrencyInput.activeFocus ? (t.accent) : "transparent"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
TextInput {
id: addCurrencyInput
anchors { fill: parent; leftMargin: 10; rightMargin: 10; topMargin: 7; bottomMargin: 7 }
color: t.cryptoPopupText
font.pixelSize: 12; font.family: "monospace"
selectionColor: t.accent
clip: true
Text {
anchors.fill: parent
text: "e.g. usd, eur, brl"
color: t.cryptoPlaceholder
font.pixelSize: 12; font.family: "monospace"
visible: parent.text === "" && !parent.activeFocus
verticalAlignment: Text.AlignVCenter
}
onAccepted: {
const c = addCoinInput.text.trim().toLowerCase()
const cur = addCurrencyInput.text.trim().toLowerCase() || root.localVsCur
if (c !== "") {
root.localPairs = root.localPairs.concat([{ coin: c, currency: cur }])
addCoinInput.text = ""
addCurrencyInput.text = ""
}
}
}
}
Rectangle {
width: 30; height: 30; radius: 8
color: addPairMouse.containsMouse
? (addPairMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
: (t.cryptoFieldBg)
Behavior on color { ColorAnimation { duration: 100 } }
Text {
anchors.centerIn: parent; text: "+"
font.pixelSize: 14; font.bold: true
color: t.cryptoPopupText
}
MouseArea {
id: addPairMouse
anchors.fill: parent; hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const c = addCoinInput.text.trim().toLowerCase()
const cur = addCurrencyInput.text.trim().toLowerCase() || root.localVsCur
if (c !== "") {
root.localPairs = root.localPairs.concat([{ coin: c, currency: cur }])
addCoinInput.text = ""
addCurrencyInput.text = ""
}
}
}
}
}
}
}
StepRow {
label: "Decimals in label"
value: root.localDecimals
minVal: 0
maxVal: 8
onStepped: (v) => root.localDecimals = v
}
RowLayout {
Layout.fillWidth: true
Text {
text: "Refresh (seconds)"
color: t.cryptoPopupDim
font.pixelSize: 12
Layout.fillWidth: true
}
Rectangle {
width: 24; height: 24; radius: 7
color: decMouse.containsMouse
? (decMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
: (t.cryptoFieldBg)
Behavior on color { ColorAnimation { duration: 100 } }
Text { anchors.centerIn: parent; text: ""; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText }
MouseArea {
id: decMouse
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: if (root.localRefreshSec > 10) root.localRefreshSec -= 1
}
}
Rectangle {
width: 54; height: 24; radius: 6
color: refreshInput.activeFocus ? (t.cryptoFieldBgFocus) : (t.cryptoFieldBg)
border.color: refreshInput.activeFocus ? (t.accent) : "transparent"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Behavior on border.color { ColorAnimation { duration: 120 } }
TextInput {
id: refreshInput
anchors { fill: parent; leftMargin: 6; rightMargin: 6; topMargin: 4; bottomMargin: 4 }
text: root.localRefreshSec.toString()
color: t.cryptoPopupText
font.pixelSize: 12
font.bold: true
font.family: "monospace"
selectionColor: t.accent
horizontalAlignment: TextInput.AlignHCenter
inputMethodHints: Qt.ImhDigitsOnly
validator: IntValidator { bottom: 10; top: 3600 }
clip: true
onActiveFocusChanged: if (!activeFocus) {
const v = parseInt(text)
if (!isNaN(v) && v >= 10 && v <= 3600)
root.localRefreshSec = v
else
text = root.localRefreshSec.toString()
}
onAccepted: {
const v = parseInt(text)
if (!isNaN(v) && v >= 10 && v <= 3600)
root.localRefreshSec = v
else
text = root.localRefreshSec.toString()
focus = false
}
Connections {
target: root
function onLocalRefreshSecChanged() {
if (!refreshInput.activeFocus)
refreshInput.text = root.localRefreshSec.toString()
}
}
}
}
Rectangle {
width: 24; height: 24; radius: 7
color: incMouse.containsMouse
? (incMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
: (t.cryptoFieldBg)
Behavior on color { ColorAnimation { duration: 100 } }
Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText }
MouseArea {
id: incMouse
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: if (root.localRefreshSec < 3600) root.localRefreshSec += 1
}
}
}
ToggleRow {
label: "Show icon on bar"
checked: root.localShowIcon
onToggled: (v) => root.localShowIcon = v
}
Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator }
Rectangle {
Layout.fillWidth: true
height: 32
radius: 9
color: applyMouse.containsMouse
? (applyMouse.pressed
? Qt.darker(t.accent, 1.15)
: (t.accent))
: (t.cryptoApplyIdleBg)
Behavior on color { ColorAnimation { duration: 130 } }
Text {
anchors.centerIn: parent
text: "Apply & Refresh"
color: applyMouse.containsMouse ? (t.cryptoTextOnAccent) : (t.cryptoPopupText)
font.pixelSize: 12
font.bold: true
Behavior on color { ColorAnimation { duration: 130 } }
}
MouseArea {
id: applyMouse
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
onClicked: {
root.price = -1
root.activeCoinIndex = 0
root.coinSymbol = root.localCoinId.toUpperCase().slice(0, 4)
root.persistSettings()
root.fetchPrice()
cryptoPopup.activeTab = 0
}
}
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "Reset to defaults"
color: t.cryptoPopupDim
font.pixelSize: 11
font.underline: true
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: {
root.localPairs = Array.isArray(cfg.cryptoPairs) && cfg.cryptoPairs.length > 0 ? cfg.cryptoPairs : [{ coin: "bitcoin", currency: "usd" }]
root.localVsCur = "usd"
root.displayCurrency = root.localPairs.length > 0 ? root.localPairs[0].currency : "usd"
root.activeCoinIndex = 0
root.localDecimals = cfg.cryptoDecimals
root.localRefreshSec = cfg.cryptoRefreshSec
root.localShowIcon = cfg.cryptoShowIcon
root.persistSettings()
root.price = -1
cryptoPopup.activeTab = 0
}
}
}
Item { height: 2 }
}
}
}
}
}