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 readonly property string configPath: Quickshell.env("HOME") + "/.config/quickshell/config/config.qml" Process { id: cryptoConfigWriter running: false onExited: (code) => { if (code !== 0) console.warn("[Crypto] Failed to patch config.qml (exit " + code + ")") } } width: chipRow.implicitWidth + 24 height: cfg.moduleHeight radius: panelRadius color: hoverHandler.hovered ? (t.cryptoBgHover ?? "#15ffffff") : (t.cryptoBg ?? "transparent") border.color: (hoverHandler.hovered || cryptoPopup.popupOpen) ? (t.cryptoBorderHover ?? t.accent ?? "#ffc19375") : (t.cryptoBorder ?? "transparent") border.width: borderWidth Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } } Behavior on color { ColorAnimation { duration: 150 } } Behavior on border.color { ColorAnimation { duration: 150 } } 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 ?? 1 property int localRefreshSec: cfg.cryptoRefreshSec ?? 60 property bool localShowIcon: cfg.cryptoShowIcon ?? true 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 ?? 1 root.localRefreshSec = cfg.cryptoRefreshSec ?? 60 root.localShowIcon = cfg.cryptoShowIcon ?? true } function persistSettings() { const pairs = root.localPairs const decimals = Math.round(root.localDecimals) const refreshSec = Math.round(root.localRefreshSec) const showIcon = root.localShowIcon const cfgPath = root.configPath const pairsQml = "[" + pairs.map(p => '{ coin: "' + p.coin + '", currency: "' + p.currency + '" }' ).join(", ") + "]" const sh = "CFG=" + JSON.stringify(cfgPath) + "\n" + "sed -i" + " -e 's/\\(property int[ ]\\+cryptoDecimals:[ ]\\+\\)[0-9.]\\+/\\1" + decimals.toString() + "/'" + " -e 's/\\(property int[ ]\\+cryptoRefreshSec:[ ]\\+\\)[0-9.]\\+/\\1" + refreshSec.toString() + "/'" + " -e 's/\\(property bool[ ]\\+cryptoShowIcon:[ ]\\+\\)\\(true\\|false\\)/\\1" + (showIcon ? "true" : "false") + "/'" + " \"$CFG\"\n" + "QS_PAIRS='" + pairsQml + "'\n" + "awk -v pairs=\"$QS_PAIRS\" '" + "/property var cryptoPairs:/{print \" property var cryptoPairs: \"pairs;sk=/\\]/?0:1;next}" + "sk&&/\\]/{sk=0;next}" + "sk{next}" + "{print}'" + " \"$CFG\" > \"${CFG}.tmp\" && mv \"${CFG}.tmp\" \"$CFG\"\n" cryptoConfigWriter.command = ["/bin/sh", "-c", sh] cryptoConfigWriter.running = true } 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 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.onreadystatechange = function () { if (xhr.readyState !== XMLHttpRequest.DONE) return loading = false if (xhr.status !== 200) { hasError = true; return } try { const data = JSON.parse(xhr.responseText) if (!data || data.length === 0) { hasError = true; return } const coin = data[0] price = coin.current_price ?? -1 change24h = coin.price_change_percentage_24h ?? 0 high24h = coin.high_24h ?? 0 low24h = coin.low_24h ?? 0 marketCap = coin.market_cap ?? 0 volume24h = coin.total_volume ?? 0 rank = coin.market_cap_rank ?? 0 circulatingSupply = coin.circulating_supply ?? 0 ath = coin.ath ?? 0 athChange = coin.ath_change_percentage ?? 0 coinSymbol = (coin.symbol ?? localCoinId).toUpperCase() coinName = coin.name ?? "" 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() HoverHandler { id: hoverHandler cursorShape: Qt.PointingHandCursor } TapHandler { onTapped: cryptoPopup.toggle() } 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 ?? t.accent ?? "#ffc19375" font.pixelSize: 14 Layout.alignment: Qt.AlignVCenter Layout.rightMargin: 7 } Text { text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": " color: t.cryptoLabel ?? t.textDim ?? "#997a8996" font.pixelSize: 12 verticalAlignment: Text.AlignVCenter } Text { text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price) color: t.cryptoValue ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 13 font.bold: true verticalAlignment: Text.AlignVCenter } Text { visible: root.price >= 0 text: root.change24h >= 0 ? "▲" : "▼" color: root.change24h >= 0 ? (t.cryptoUp ?? "#ffb4c9a1") : (t.cryptoDown ?? "#fff38ba8") font.pixelSize: 9 verticalAlignment: Text.AlignVCenter Layout.alignment: Qt.AlignVCenter Layout.leftMargin: 4 Behavior on color { ColorAnimation { duration: 200 } } } } PopupPanel { id: cryptoPopup ns: "crypto" chipRoot: root cardWidth: 300 hasKeyboardFocus: true function open() { popupCenterX = root.mapToItem(null, 0, 0).x + root.width / 2 _doOpen() } Rectangle { id: card width: parent.width height: cardCol.implicitHeight + 24 anchors.top: root.isTop ? parent.top : undefined anchors.bottom: root.isTop ? undefined : parent.bottom transform: Translate { y: cryptoPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height) Behavior on y { NumberAnimation { duration: Cfg.Config.popupAnimDuration; easing.type: Easing.OutExpo } enabled: cryptoPopup.visible } } opacity: cryptoPopup.popupOpen ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 200 } } Behavior on height { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } color: t.cryptoPopupBg ?? t.calCardBg ?? "#cc0c0e12" radius: Cfg.Config.popupRadius border.color: t.cryptoPopupBorder ?? t.calCardBorder ?? "transparent" border.width: Cfg.Config.popupBorderWidth layer.enabled: true MouseArea { anchors.fill: parent; propagateComposedEvents: false } property int activeTab: 0 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: "#15ffffff" 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: card.activeTab === tabIndex color: active ? (t.accent ?? "#ffc19375") : (tm.containsMouse ? "#22ffffff" : "transparent") Behavior on color { ColorAnimation { duration: 130 } } Text { anchors.centerIn: parent text: parent.label color: parent.active ? "#ff0c0e12" : (t.cryptoPopupDim ?? t.textDim ?? "#997a8996") 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: card.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: card.activeTab === 0 ? 0 : -tabContainer.width opacity: card.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 ?? t.accent ?? "#ffc19375" 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 ?? t.clockPopupHeader ?? "#ffc19375" font.pixelSize: 15 font.bold: true } Text { text: root.coinName !== "" ? root.coinName : (root.localCoinId.charAt(0).toUpperCase() + root.localCoinId.slice(1)) color: t.cryptoPopupDim ?? t.textDim ?? "#997a8996" font.pixelSize: 11 } } Rectangle { width: badge.implicitWidth + 12 height: 22; radius: 11 color: root.change24h >= 0 ? "#22b4c9a1" : "#22f38ba8" Text { id: badge anchors.centerIn: parent text: (root.change24h >= 0 ? "▲ +" : "▼ ") + root.change24h.toFixed(2) + "%" color: root.change24h >= 0 ? (t.cryptoUp ?? "#ffb4c9a1") : (t.cryptoDown ?? "#fff38ba8") 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 ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 24 font.bold: true font.family: "monospace" } Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator ?? "#334a5d6e" } component StatRow: RowLayout { property string label: "" property string value: "" Layout.fillWidth: true Text { text: label; color: t.cryptoPopupDim ?? t.textDim ?? "#997a8996"; font.pixelSize: 12; Layout.fillWidth: true } Text { text: value; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2"; 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 ?? t.textDim ?? "#997a8996"; font.pixelSize: 12; Layout.fillWidth: true } Text { text: root.ath > 0 ? root.fullPrice(root.ath) : "—" color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 12; font.bold: true; font.family: "monospace" } Text { visible: root.ath > 0 text: "(" + root.athChange.toFixed(2) + "%)" color: t.cryptoDown ?? "#fff38ba8" 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 ?? t.textDim ?? "#997a8996" 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 ?? t.textDim ?? "#997a8996" font.pixelSize: 11 } } Item { Layout.fillWidth: true height: 6 Rectangle { anchors.fill: parent radius: 3 color: "#1affffff" } Rectangle { height: parent.height radius: 3 color: root.change24h >= 0 ? (t.cryptoUp ?? "#ffb4c9a1") : (t.cryptoDown ?? "#fff38ba8") 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 ?? "#fff38ba8" font.pixelSize: 10 font.family: "monospace" } Item { Layout.fillWidth: true } Text { text: root.high24h > 0 ? root.fullPrice(root.high24h) : "—" color: t.cryptoUp ?? "#ffb4c9a1" font.pixelSize: 10 font.family: "monospace" } } } Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator ?? "#334a5d6e" } RowLayout { Layout.fillWidth: true Text { text: root.hasError ? "⚠ fetch error" : "Updated " + (root.lastUpdated || "—") color: t.cryptoPopupDim ?? t.textDim ?? "#997a8996" font.pixelSize: 11 Layout.fillWidth: true } Rectangle { id: refreshBtn width: 26; height: 26; radius: 8 color: rMouse.containsMouse ? (rMouse.pressed ? (t.accent ?? "#ffc19375") : "#22ffffff") : "transparent" Behavior on color { ColorAnimation { duration: 120 } } Text { id: refreshIcon anchors.centerIn: parent text: "󰑐" color: t.cryptoIcon ?? t.accent ?? "#ffc19375" 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: card.activeTab === 1 ? 0 : tabContainer.width opacity: card.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 ?? t.textDim ?? "#997a8996" font.pixelSize: 11 font.capitalization: Font.AllUppercase } Rectangle { Layout.fillWidth: true height: 30 radius: 8 color: fi.activeFocus ? "#22ffffff" : "#15ffffff" border.color: fi.activeFocus ? (t.accent ?? "#ffc19375") : "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 ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 12 font.family: "monospace" selectionColor: t.accent ?? "#ffc19375" clip: true Text { anchors.fill: parent text: parent.parent.parent.placeholder color: "#55" + (t.textDim ?? "#7a8996").slice(1) 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 ?? t.textDim ?? "#997a8996"; font.pixelSize: 12; Layout.fillWidth: true } Rectangle { width: 24; height: 24; radius: 7 color: mm.containsMouse ? (mm.pressed ? (t.accent ?? "#ffc19375") : "#22ffffff") : "#15ffffff" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent; text: "−"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" } 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: "#15ffffff" Text { anchors.centerIn: parent; text: value; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2"; font.pixelSize: 12; font.bold: true; font.family: "monospace" } } Rectangle { width: 24; height: 24; radius: 7 color: pm.containsMouse ? (pm.pressed ? (t.accent ?? "#ffc19375") : "#22ffffff") : "#15ffffff" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" } 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 ?? t.textDim ?? "#997a8996"; font.pixelSize: 12; Layout.fillWidth: true } Rectangle { width: 36; height: 20; radius: 10 color: checked ? (t.accent ?? "#ffc19375") : "#22ffffff" 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 ?? t.textDim ?? "#997a8996" 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 ?? "#cc0c0e12" } 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 ?? "#cc0c0e12" } } 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 ?? "#ffc19375") : "#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 ?? "#ffc19375") : (t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2") 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 ? "#44f38ba8" : "transparent" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent text: "×"; font.pixelSize: 11; font.bold: true color: t.cryptoDown ?? "#fff38ba8" } 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: "#15ffffff" } Rectangle { height: parent.height radius: 2 color: t.accent ?? "#ffc19375" 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 ?? t.textDim ?? "#997a8996" font.pixelSize: 11 font.capitalization: Font.AllUppercase } Rectangle { Layout.fillWidth: true height: 30; radius: 8 color: addCoinInput.activeFocus ? "#22ffffff" : "#15ffffff" border.color: addCoinInput.activeFocus ? (t.accent ?? "#ffc19375") : "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 ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 12; font.family: "monospace" selectionColor: t.accent ?? "#ffc19375" clip: true Text { anchors.fill: parent text: "e.g. bitcoin, monero, solana" color: "#55" + (t.textDim ?? "#7a8996").slice(1) 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 ?? t.textDim ?? "#997a8996" font.pixelSize: 11 font.capitalization: Font.AllUppercase } RowLayout { Layout.fillWidth: true spacing: 6 Rectangle { Layout.fillWidth: true height: 30; radius: 8 color: addCurrencyInput.activeFocus ? "#22ffffff" : "#15ffffff" border.color: addCurrencyInput.activeFocus ? (t.accent ?? "#ffc19375") : "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 ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 12; font.family: "monospace" selectionColor: t.accent ?? "#ffc19375" clip: true Text { anchors.fill: parent text: "e.g. usd, eur, brl" color: "#55" + (t.textDim ?? "#7a8996").slice(1) 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 ?? "#ffc19375") : "#22ffffff") : "#15ffffff" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent; text: "+" font.pixelSize: 14; font.bold: true color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" } 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 ?? t.textDim ?? "#997a8996" font.pixelSize: 12 Layout.fillWidth: true } Rectangle { width: 24; height: 24; radius: 7 color: decMouse.containsMouse ? (decMouse.pressed ? (t.accent ?? "#ffc19375") : "#22ffffff") : "#15ffffff" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent; text: "−"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" } 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 ? "#22ffffff" : "#15ffffff" border.color: refreshInput.activeFocus ? (t.accent ?? "#ffc19375") : "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 ?? t.textMain ?? "#ffe8eef2" font.pixelSize: 12 font.bold: true font.family: "monospace" selectionColor: t.accent ?? "#ffc19375" 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 ?? "#ffc19375") : "#22ffffff") : "#15ffffff" Behavior on color { ColorAnimation { duration: 100 } } Text { anchors.centerIn: parent; text: "+"; font.pixelSize: 14; font.bold: true; color: t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2" } 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 ?? "#334a5d6e" } Rectangle { Layout.fillWidth: true height: 32 radius: 9 color: applyMouse.containsMouse ? (applyMouse.pressed ? Qt.darker(t.accent ?? "#ffc19375", 1.15) : (t.accent ?? "#ffc19375")) : (t.cryptoApplyIdleBg ?? "#33c19375") Behavior on color { ColorAnimation { duration: 130 } } Text { anchors.centerIn: parent text: "Apply & Refresh" color: applyMouse.containsMouse ? "#ff0c0e12" : (t.cryptoPopupText ?? t.textMain ?? "#ffe8eef2") 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() card.activeTab = 0 } } } Text { Layout.alignment: Qt.AlignHCenter text: "Reset to defaults" color: t.cryptoPopupDim ?? t.textDim ?? "#997a8996" 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 ?? 1 root.localRefreshSec = cfg.cryptoRefreshSec ?? 60 root.localShowIcon = cfg.cryptoShowIcon ?? true root.persistSettings() root.price = -1 card.activeTab = 0 } } } Item { height: 2 } } } } } } }