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 } } } } } }