1440 lines
73 KiB
QML
1440 lines
73 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
|
||
|
||
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 int localRotateSec: cfg.cryptoRotateSec
|
||
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.localRotateSec = cfg.cryptoRotateSec
|
||
root.localShowIcon = cfg.cryptoShowIcon
|
||
}
|
||
|
||
function persistSettings() {
|
||
const pairs = root.localPairs
|
||
const decimals = Math.round(root.localDecimals)
|
||
const refreshSec = Math.round(root.localRefreshSec)
|
||
const rotateSec = Math.round(root.localRotateSec)
|
||
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 },
|
||
{ key: "cryptoRotateSec", value: rotateSec }
|
||
])
|
||
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
|
||
property var sparklineData: []
|
||
property real change7d: 0
|
||
property var _priceCache: ({})
|
||
property int _fetchGen: 0
|
||
|
||
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 rotatePrev() {
|
||
if (root.localPairs.length <= 1) return
|
||
root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length
|
||
root._applyFromCache()
|
||
rotateTimer.restart()
|
||
}
|
||
|
||
function rotateNext() {
|
||
if (root.localPairs.length <= 1) return
|
||
root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length
|
||
root._applyFromCache()
|
||
rotateTimer.restart()
|
||
}
|
||
|
||
function _applyFromCache() {
|
||
var key = root.localCoinId + "_" + root.localActiveCurrency
|
||
var entry = root._priceCache[key]
|
||
if (!entry) { console.log("Crypto: no cache entry for", key); return }
|
||
console.log("Crypto: applying", key, "price=" + entry.price)
|
||
price = entry.price
|
||
change24h = entry.change24h
|
||
high24h = entry.high24h
|
||
low24h = entry.low24h
|
||
marketCap = entry.marketCap
|
||
volume24h = entry.volume24h
|
||
rank = entry.rank
|
||
circulatingSupply = entry.circulatingSupply
|
||
ath = entry.ath
|
||
athChange = entry.athChange
|
||
coinSymbol = safeString(entry.symbol, 8, root.localCoinId).toUpperCase()
|
||
coinName = safeString(entry.name, 50, "")
|
||
lastUpdated = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
||
root.displayCurrency = root.localActiveCurrency
|
||
sparklineData = entry.sparkline || []
|
||
change7d = entry.change7d || 0
|
||
hasError = false
|
||
}
|
||
|
||
function fetchPrice() {
|
||
loading = true
|
||
hasError = false
|
||
_fetchGen++
|
||
var myGen = _fetchGen
|
||
|
||
var coinGroups = {}
|
||
for (var i = 0; i < root.localPairs.length; i++) {
|
||
var p = root.localPairs[i]
|
||
if (!coinGroups[p.currency]) coinGroups[p.currency] = []
|
||
var coinList = coinGroups[p.currency]
|
||
if (coinList.indexOf(p.coin) === -1) coinList.push(p.coin)
|
||
}
|
||
|
||
var currencies = Object.keys(coinGroups)
|
||
var pending = currencies.length
|
||
|
||
if (pending === 0) { loading = false; hasError = true; return }
|
||
|
||
for (var ci = 0; ci < currencies.length; ci++) {
|
||
var cur = currencies[ci]
|
||
var ids = coinGroups[cur].join(",")
|
||
|
||
var url =
|
||
"https://api.coingecko.com/api/v3/coins/markets" +
|
||
"?vs_currency=" + encodeURIComponent(cur || "usd") +
|
||
"&ids=" + encodeURIComponent(ids) +
|
||
"&order=market_cap_desc&per_page=250&page=1" +
|
||
"&sparkline=true&price_change_percentage=24h,7d"
|
||
|
||
var xhr = new XMLHttpRequest()
|
||
xhr._currency = cur
|
||
xhr.timeout = 15000
|
||
xhr.onreadystatechange = (function(xhr, cur, myGen) {
|
||
return function() {
|
||
if (myGen !== root._fetchGen) return
|
||
if (xhr.readyState !== XMLHttpRequest.DONE) return
|
||
if (xhr.status !== 200) { hasError = true }
|
||
else {
|
||
try {
|
||
var text = xhr.responseText
|
||
if (text.length <= 65536) {
|
||
var data = JSON.parse(text)
|
||
if (Array.isArray(data)) {
|
||
for (var di = 0; di < data.length; di++) {
|
||
var coin = data[di]
|
||
var key = coin.id + "_" + cur
|
||
console.log("Crypto: cache", key, "price=" + coin.current_price)
|
||
root._priceCache[key] = {
|
||
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),
|
||
symbol: safeString(coin.symbol, 8, coin.id),
|
||
name: safeString(coin.name, 50, ""),
|
||
sparkline: (coin.sparkline_in_7d && Array.isArray(coin.sparkline_in_7d.price))
|
||
? coin.sparkline_in_7d.price : [],
|
||
change7d: safeNumber(coin.price_change_percentage_7d_in_currency, 0)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
if (--pending <= 0) {
|
||
loading = false
|
||
root._applyFromCache()
|
||
}
|
||
}
|
||
})(xhr, cur, myGen)
|
||
|
||
xhr.open("GET", url)
|
||
xhr.setRequestHeader("Accept", "application/json")
|
||
xhr.send()
|
||
}
|
||
}
|
||
|
||
Component.onCompleted: { resolveWithPersistence(); fetchPrice() }
|
||
|
||
Timer {
|
||
id: rotateTimer
|
||
interval: root.localRotateSec * 1000
|
||
running: root.localRotateSec > 0 && root.localPairs.length > 1
|
||
repeat: true
|
||
onTriggered: root.rotateNext()
|
||
}
|
||
|
||
Timer {
|
||
id: refreshTimer
|
||
interval: root.localRefreshSec * 1000
|
||
running: true
|
||
repeat: true
|
||
triggeredOnStart: true
|
||
onTriggered: root.fetchPrice()
|
||
}
|
||
|
||
onLocalRefreshSecChanged: refreshTimer.restart()
|
||
onLocalRotateSecChanged: rotateTimer.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
|
||
font.bold: true
|
||
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
|
||
font.bold: true
|
||
renderType: Text.NativeRendering
|
||
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: 36
|
||
radius: 12
|
||
color: t.cryptoFieldBg
|
||
|
||
RowLayout {
|
||
anchors.fill: parent
|
||
anchors.margins: 4
|
||
spacing: 4
|
||
|
||
component TabBtn: Rectangle {
|
||
required property string label
|
||
required property int tabIndex
|
||
property string icon: ""
|
||
Layout.fillWidth: true
|
||
height: 28
|
||
radius: 9
|
||
|
||
readonly property bool active: cryptoPopup.activeTab === tabIndex
|
||
|
||
color: active
|
||
? (t.accent)
|
||
: (tm.containsMouse ? (t.cryptoFieldBgFocus) : "transparent")
|
||
Behavior on color { ColorAnimation { duration: 150 } }
|
||
|
||
border.color: active ? Qt.rgba(1, 1, 1, 0.18) : "transparent"
|
||
border.width: 1
|
||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||
|
||
scale: tm.pressed ? 0.93 : 1.0
|
||
Behavior on scale { NumberAnimation { duration: 90; easing.type: Easing.OutQuad } }
|
||
|
||
Row {
|
||
anchors.centerIn: parent
|
||
spacing: 5
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
visible: parent.parent.icon !== ""
|
||
text: parent.parent.icon
|
||
color: parent.parent.active ? (t.cryptoTextOnAccent) : (t.cryptoPopupDim)
|
||
font.pixelSize: 11
|
||
opacity: parent.parent.active ? 1.0 : 0.7
|
||
Behavior on color { ColorAnimation { duration: 150 } }
|
||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||
}
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: parent.parent.label
|
||
color: parent.parent.active ? (t.cryptoTextOnAccent) : (t.cryptoPopupDim)
|
||
font.pixelSize: 12
|
||
font.bold: parent.parent.active
|
||
font.letterSpacing: parent.parent.active ? 0.4 : 0.0
|
||
Behavior on color { ColorAnimation { duration: 150 } }
|
||
Behavior on font.letterSpacing { NumberAnimation { duration: 150 } }
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: tm
|
||
anchors.fill: parent; hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: cryptoPopup.activeTab = tabIndex
|
||
}
|
||
}
|
||
|
||
TabBtn { label: "Price"; icon: ""; tabIndex: 0 }
|
||
TabBtn { label: "Settings"; icon: ""; 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
|
||
renderType: Text.NativeRendering
|
||
}
|
||
}
|
||
}
|
||
|
||
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"; renderType: Text.NativeRendering }
|
||
}
|
||
|
||
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
|
||
renderType: Text.NativeRendering
|
||
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
|
||
renderType: Text.NativeRendering
|
||
}
|
||
}
|
||
|
||
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"
|
||
renderType: Text.NativeRendering
|
||
}
|
||
Item { Layout.fillWidth: true }
|
||
Text {
|
||
text: root.high24h > 0 ? root.fullPrice(root.high24h) : "—"
|
||
color: t.cryptoUp
|
||
font.pixelSize: 10
|
||
font.family: "monospace"
|
||
renderType: Text.NativeRendering
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator }
|
||
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 5
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
Text {
|
||
text: "7d Chart"
|
||
color: t.cryptoPopupDim
|
||
font.pixelSize: 12
|
||
Layout.fillWidth: true
|
||
}
|
||
Rectangle {
|
||
visible: root.sparklineData.length > 1
|
||
width: badge7d.implicitWidth + 12
|
||
height: 20; radius: 10
|
||
color: root.change7d >= 0 ? (t.cryptoUpBg) : (t.cryptoDownBg)
|
||
Text {
|
||
id: badge7d
|
||
anchors.centerIn: parent
|
||
text: (root.change7d >= 0 ? "▲ +" : "▼ ") + root.change7d.toFixed(2) + "%"
|
||
color: root.change7d >= 0 ? (t.cryptoUp) : (t.cryptoDown)
|
||
font.pixelSize: 10
|
||
font.bold: true
|
||
}
|
||
}
|
||
}
|
||
|
||
Canvas {
|
||
id: sparklineCanvas
|
||
Layout.fillWidth: true
|
||
height: 52
|
||
|
||
property var prices: root.sparklineData
|
||
property bool isUp: root.change7d >= 0
|
||
property string upColor: t.cryptoUp || "#4ade80"
|
||
property string downColor: t.cryptoDown || "#f87171"
|
||
|
||
onPricesChanged: requestPaint()
|
||
onIsUpChanged: requestPaint()
|
||
onUpColorChanged: requestPaint()
|
||
onDownColorChanged:requestPaint()
|
||
|
||
onPaint: {
|
||
var ctx = getContext("2d")
|
||
ctx.clearRect(0, 0, width, height)
|
||
var pts = prices
|
||
if (!pts || pts.length < 2) {
|
||
ctx.fillStyle = Qt.rgba(1, 1, 1, 0.06)
|
||
ctx.beginPath()
|
||
ctx.roundedRect(0, 0, width, height, 6, 6)
|
||
ctx.fill()
|
||
return
|
||
}
|
||
var minP = pts[0], maxP = pts[0]
|
||
for (var i = 1; i < pts.length; i++) {
|
||
if (pts[i] < minP) minP = pts[i]
|
||
if (pts[i] > maxP) maxP = pts[i]
|
||
}
|
||
var range = maxP - minP
|
||
if (range === 0) range = 1
|
||
var pad = 4, pw = width - pad * 2, ph = height - pad * 2
|
||
var lineColor = isUp ? upColor : downColor
|
||
|
||
ctx.beginPath()
|
||
for (var j = 0; j < pts.length; j++) {
|
||
var x = pad + (j / (pts.length - 1)) * pw
|
||
var y = pad + ph - ((pts[j] - minP) / range) * ph
|
||
if (j === 0) ctx.moveTo(x, y)
|
||
else ctx.lineTo(x, y)
|
||
}
|
||
ctx.lineTo(pad + pw, pad + ph)
|
||
ctx.lineTo(pad, pad + ph)
|
||
ctx.closePath()
|
||
var grad = ctx.createLinearGradient(0, 0, 0, height)
|
||
grad.addColorStop(0, Qt.rgba(
|
||
isUp ? 0.29 : 0.97,
|
||
isUp ? 0.87 : 0.44,
|
||
isUp ? 0.50 : 0.44, 0.22))
|
||
grad.addColorStop(1, Qt.rgba(0, 0, 0, 0))
|
||
ctx.fillStyle = grad
|
||
ctx.fill()
|
||
|
||
ctx.beginPath()
|
||
for (var k = 0; k < pts.length; k++) {
|
||
var lx = pad + (k / (pts.length - 1)) * pw
|
||
var ly = pad + ph - ((pts[k] - minP) / range) * ph
|
||
if (k === 0) ctx.moveTo(lx, ly)
|
||
else ctx.lineTo(lx, ly)
|
||
}
|
||
ctx.strokeStyle = lineColor
|
||
ctx.lineWidth = 1.5
|
||
ctx.lineJoin = "round"
|
||
ctx.stroke()
|
||
|
||
var lx2 = pad + pw
|
||
var ly2 = pad + ph - ((pts[pts.length - 1] - minP) / range) * ph
|
||
ctx.beginPath()
|
||
ctx.arc(lx2, ly2, 3, 0, Math.PI * 2)
|
||
ctx.fillStyle = lineColor
|
||
ctx.fill()
|
||
}
|
||
}
|
||
}
|
||
|
||
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 {
|
||
visible: root.localPairs.length > 1
|
||
width: 26; height: 26; radius: 8
|
||
color: prevMouse.containsMouse
|
||
? (prevMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
|
||
: "transparent"
|
||
Behavior on color { ColorAnimation { duration: 120 } }
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "‹"
|
||
color: t.cryptoIcon
|
||
font.pixelSize: 14
|
||
}
|
||
|
||
MouseArea {
|
||
id: prevMouse
|
||
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.rotatePrev()
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
visible: root.localPairs.length > 1
|
||
width: 26; height: 26; radius: 8
|
||
color: nextMouse.containsMouse
|
||
? (nextMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
|
||
: "transparent"
|
||
Behavior on color { ColorAnimation { duration: 120 } }
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "›"
|
||
color: t.cryptoIcon
|
||
font.pixelSize: 14
|
||
}
|
||
|
||
MouseArea {
|
||
id: nextMouse
|
||
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
|
||
onClicked: root.rotateNext()
|
||
}
|
||
}
|
||
|
||
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
|
||
height: tabContainer.height
|
||
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: 8
|
||
|
||
Text {
|
||
text: "Pairs to cycle"
|
||
color: t.cryptoPopupDim
|
||
font.pixelSize: 11
|
||
font.capitalization: Font.AllUppercase
|
||
}
|
||
|
||
ColumnLayout {
|
||
Layout.fillWidth: true
|
||
spacing: 8
|
||
|
||
Item {
|
||
Layout.fillWidth: true
|
||
height: 28
|
||
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
|
||
root.fetchPrice()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 = ""
|
||
root.fetchPrice()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
Layout.fillWidth: true
|
||
Layout.topMargin: 4
|
||
height: 32; radius: 9
|
||
color: addPairMouse.containsMouse
|
||
? (addPairMouse.pressed ? (t.accent) : (t.cryptoFieldBgFocus))
|
||
: (t.cryptoApplyIdleBg)
|
||
Behavior on color { ColorAnimation { duration: 130 } }
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "+ Add Pair"
|
||
color: addPairMouse.containsMouse ? (t.cryptoTextOnAccent) : (t.cryptoPopupText)
|
||
font.pixelSize: 12; font.bold: true
|
||
Behavior on color { ColorAnimation { duration: 130 } }
|
||
}
|
||
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 = ""
|
||
root.fetchPrice()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
Item { Layout.fillHeight: true }
|
||
|
||
Rectangle { Layout.fillWidth: true; height: 1; color: t.calSeparator }
|
||
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|
||
RowLayout {
|
||
Layout.fillWidth: true
|
||
|
||
Text {
|
||
text: "Rotate (seconds)"
|
||
color: t.cryptoPopupDim
|
||
font.pixelSize: 12
|
||
Layout.fillWidth: true
|
||
}
|
||
|
||
Rectangle {
|
||
width: 24; height: 24; radius: 7
|
||
color: rotDecMouse.containsMouse
|
||
? (rotDecMouse.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: rotDecMouse
|
||
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
|
||
onClicked: if (root.localRotateSec > 1) root.localRotateSec -= 1
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: 54; height: 24; radius: 6
|
||
color: rotateInput.activeFocus ? (t.cryptoFieldBgFocus) : (t.cryptoFieldBg)
|
||
border.color: rotateInput.activeFocus ? (t.accent) : "transparent"
|
||
border.width: 1
|
||
Behavior on color { ColorAnimation { duration: 120 } }
|
||
Behavior on border.color { ColorAnimation { duration: 120 } }
|
||
|
||
TextInput {
|
||
id: rotateInput
|
||
anchors { fill: parent; leftMargin: 6; rightMargin: 6; topMargin: 4; bottomMargin: 4 }
|
||
text: root.localRotateSec.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: 1; top: 600 }
|
||
clip: true
|
||
|
||
onActiveFocusChanged: if (!activeFocus) {
|
||
const v = parseInt(text)
|
||
if (!isNaN(v) && v >= 1 && v <= 600)
|
||
root.localRotateSec = v
|
||
else
|
||
text = root.localRotateSec.toString()
|
||
}
|
||
onAccepted: {
|
||
const v = parseInt(text)
|
||
if (!isNaN(v) && v >= 1 && v <= 600)
|
||
root.localRotateSec = v
|
||
else
|
||
text = root.localRotateSec.toString()
|
||
focus = false
|
||
}
|
||
|
||
Connections {
|
||
target: root
|
||
function onLocalRotateSecChanged() {
|
||
if (!rotateInput.activeFocus)
|
||
rotateInput.text = root.localRotateSec.toString()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
width: 24; height: 24; radius: 7
|
||
color: rotIncMouse.containsMouse
|
||
? (rotIncMouse.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: rotIncMouse
|
||
anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor
|
||
onClicked: if (root.localRotateSec < 600) root.localRotateSec += 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.activeCoinIndex = 0
|
||
root.persistSettings()
|
||
root.fetchPrice()
|
||
cryptoPopup.activeTab = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
Item { height: 2 }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|