1135 lines
59 KiB
QML
1135 lines
59 KiB
QML
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 }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|