From 0e1f043e4c4909865c1506911263725d651e735e Mon Sep 17 00:00:00 2001 From: enne2 Date: Sun, 6 Sep 2026 18:50:33 +0200 Subject: [PATCH] fix(ui): harmonize layout with codex widget, optimize vertical spacing and cost formatting --- contents/ui/LimitRow.qml | 56 ++++++--- contents/ui/main.qml | 261 ++++++++++++++++++++------------------- 2 files changed, 173 insertions(+), 144 deletions(-) diff --git a/contents/ui/LimitRow.qml b/contents/ui/LimitRow.qml index 3240ed1..09db79f 100644 --- a/contents/ui/LimitRow.qml +++ b/contents/ui/LimitRow.qml @@ -1,4 +1,4 @@ -/* Card compatta per una quota Ollama Cloud, pensata per il dashboard desktop. */ +/* Card compatta per una quota Ollama Cloud (dashboard desktop). */ import QtQuick import QtQuick.Layouts import org.kde.plasma.components 3.0 as PC3 @@ -13,6 +13,8 @@ Rectangle { property real used: 0.0 property color accent: "#00bcd4" property var models: [] + property var reset: null + property int clockTick: 0 function requestCount() { let count = 0 @@ -24,11 +26,22 @@ Rectangle { return count >= 1000 ? (count / 1000).toFixed(1) + "K req" : count + " req" } + function formatResetRemaining(resetAt) { + const tick = root.clockTick + if (!resetAt) return "—" + let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000)) + if (mins === 0) return i18n("ora") + const days = Math.floor(mins / 1440) + mins -= days * 1440 + const hours = Math.floor(mins / 60) + mins -= hours * 60 + return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) + } + Layout.fillWidth: true - Layout.minimumWidth: 0 - Layout.preferredHeight: 42 - radius: 6 - color: Qt.rgba(0, 0, 0, 0.26) + Layout.preferredHeight: 52 + radius: 5 + color: Qt.rgba(0, 0, 0, 0.25) border.width: 1 border.color: root.accent @@ -39,27 +52,27 @@ Rectangle { RowLayout { Layout.fillWidth: true - spacing: 5 + spacing: 4 Rectangle { - Layout.preferredWidth: 25 + Layout.preferredWidth: 22 Layout.preferredHeight: 14 - radius: 8 + radius: 7 color: root.accent PC3.Label { anchors.centerIn: parent text: root.period color: "#101012" font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 } } PC3.Label { text: root.title font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 elide: Text.ElideRight Layout.fillWidth: true - Layout.minimumWidth: 0 } PC3.Label { text: root.requestLabel() @@ -70,7 +83,7 @@ Rectangle { text: Math.round(root.remaining * 100) + "%" color: root.accent font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3 + font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 } } @@ -80,7 +93,7 @@ Rectangle { Rectangle { id: track anchors.fill: parent - radius: 4 + radius: 3 color: Qt.rgba(0, 0, 0, 0.55) } Rectangle { @@ -88,10 +101,25 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter width: track.width * Math.max(0, Math.min(1, root.remaining)) height: parent.height - radius: 4 + radius: 3 color: root.accent } } + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: i18n("%1% usato", Math.round(root.used * 100)) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: i18n("Reset: %1", root.formatResetRemaining(root.reset ? root.reset.at : null)) + color: root.accent + font.bold: true + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } } -} \ No newline at end of file +} diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 97ab2d0..678546d 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -23,13 +23,14 @@ PlasmoidItem { property bool showActivity: Plasmoid.configuration.showActivity property bool showModels: Plasmoid.configuration.showModels + readonly property color brandColor: "#00bcd4" + Plasmoid.title: i18n("Ollama Cloud") Plasmoid.icon: "view-statistics" Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground - toolTipMainText: i18n("Ollama Cloud — dashboard quota") + toolTipMainText: i18n("Ollama Cloud — Rate Limits") toolTipSubText: root.toolTipSub() - // Il widget è desktop-first; la vista compatta è solo fallback per pannelli. preferredRepresentation: fullRepresentation switchWidth: 150 switchHeight: 150 @@ -73,15 +74,26 @@ PlasmoidItem { executable.connectSource("/bin/bash " + shellQuote(path)) } function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" } - function pctRemaining(limit) { - return limit && typeof limit.remaining === "number" ? Math.round(limit.remaining * 100) : 0 + + function primRemainingPct() { + return root.usage && root.usage.session ? Math.round((root.usage.session.remaining || 0) * 100) : 0 } - function totalRequests(limit) { - if (!limit || !limit.models) return 0 - let count = 0 - for (let i = 0; i < limit.models.length; ++i) count += Number(limit.models[i].request_count || 0) - return count + function primUsedPct() { + return root.usage && root.usage.session ? Math.round((root.usage.session.used || 0) * 100) : 0 } + + function secRemainingPct() { + return root.usage && root.usage.weekly ? Math.round((root.usage.weekly.remaining || 0) * 100) : 0 + } + function secUsedPct() { + return root.usage && root.usage.weekly ? Math.round((root.usage.weekly.used || 0) * 100) : 0 + } + + function effectiveRemainingPct() { + if (!root.usage) return 0 + return Math.min(primRemainingPct(), secRemainingPct()) + } + function topModel(limit) { if (!limit || !limit.models || limit.models.length === 0) return "—" let top = limit.models[0] @@ -90,42 +102,40 @@ PlasmoidItem { } return top.name } + function shortModel(limit) { const name = topModel(limit) if (name.indexOf("deepseek-v4-flash") === 0) return "DeepSeek V4" if (name.indexOf("glm-") === 0) return name.replace(/:.*/, "").toUpperCase() return name.replace(/:.*/, "") } - // Verde/ciano quando c'è margine; giallo/rosso vicino all'esaurimento. - function accentFor(pct, normalAccent) { + + function accentColorFor(pct) { if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor - return normalAccent + return root.brandColor } - function resetMoment(reset) { - const tick = root.clockTick // dipendenza: aggiorna la visualizzazione ogni 30 secondi - if (!reset || !reset.at) return "—" - const date = new Date(reset.at * 1000) - return date.toLocaleDateString(Qt.locale(), "dd/MM") + " " + date.toLocaleTimeString(Qt.locale(), "HH:mm") + + function globalAccentColor() { + if (!root.usage) return Kirigami.Theme.disabledTextColor + if (primRemainingPct() === 0 || secRemainingPct() === 0) return Kirigami.Theme.negativeTextColor + return accentColorFor(effectiveRemainingPct()) } - function resetRemaining(reset) { + + function formatResetRemaining(resetAt) { const tick = root.clockTick - if (!reset || !reset.at) return "—" - let minutes = Math.max(0, Math.floor((reset.at * 1000 - Date.now()) / 60000)) - if (minutes === 0) return i18n("ora") - const days = Math.floor(minutes / 1440) - minutes -= days * 1440 - const hours = Math.floor(minutes / 60) - minutes -= hours * 60 - return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, minutes) - } - function resetStatus() { - if (!root.usage || !root.usage.resets) return i18n("STIMA RESET") - return (root.usage.resets.session.estimated || root.usage.resets.weekly.estimated) - ? i18n("RESET · STIMA") : i18n("RESET · RILEVATO") + if (!resetAt) return "—" + let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000)) + if (mins === 0) return i18n("ora") + const days = Math.floor(mins / 1440) + mins -= days * 1440 + const hours = Math.floor(mins / 60) + mins -= hours * 60 + return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) } + function toolTipSub() { - if (root.usage) return i18n("Sessione: %1% · Settimana: %2%", pctRemaining(root.usage.session), pctRemaining(root.usage.weekly)) + if (root.usage) return i18n("Settimana (7D): %1% · Finestra 5h: %2%", secRemainingPct(), primRemainingPct()) return root.errorMsg || i18n("Aggiornamento in corso…") } @@ -143,7 +153,6 @@ PlasmoidItem { onTriggered: root.clockTick += 1 } - // Fallback minimale se fosse aggiunto accidentalmente a un pannello. compactRepresentation: Component { Item { implicitWidth: Kirigami.Units.iconSizes.smallMedium @@ -151,7 +160,7 @@ PlasmoidItem { Kirigami.Icon { anchors.fill: parent source: Plasmoid.icon - color: root.usage ? root.accentFor(Math.min(root.pctRemaining(root.usage.session), root.pctRemaining(root.usage.weekly)), "#00bcd4") : Kirigami.Theme.disabledTextColor + color: root.usage ? root.globalAccentColor() : Kirigami.Theme.disabledTextColor } MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded } } @@ -166,42 +175,47 @@ PlasmoidItem { ColumnLayout { anchors.fill: parent - anchors.margins: 8 + anchors.margins: 7 spacing: 4 - // Header con stato della connessione. + // Header RowLayout { Layout.fillWidth: true + spacing: 5 Kirigami.Icon { source: Plasmoid.icon - color: "#00bcd4" - Layout.preferredWidth: 20 - Layout.preferredHeight: 20 + color: root.brandColor + Layout.preferredWidth: 18 + Layout.preferredHeight: 18 } ColumnLayout { spacing: 0 PC3.Label { text: i18n("OLLAMA CLOUD") font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 + font.pointSize: Kirigami.Theme.defaultFont.pointSize } PC3.Label { - text: root.errorMsg ? i18n("OFFLINE") : i18n("QUOTA DASHBOARD") - color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#00bcd4" + text: root.errorMsg ? i18n("OFFLINE") : i18n("CLOUD") + color: root.brandColor font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 } } Item { Layout.fillWidth: true } + Rectangle { - Layout.preferredWidth: 9 - Layout.preferredHeight: 9 - radius: 5 - color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#7cb342" + Layout.preferredWidth: 8 + Layout.preferredHeight: 8 + radius: 4 + color: root.errorMsg ? Kirigami.Theme.negativeTextColor : + (root.usage && (root.secRemainingPct() === 0 || root.primRemainingPct() === 0) ? Kirigami.Theme.negativeTextColor : root.brandColor) } PC3.Button { icon.name: "view-refresh" display: QQC2.AbstractButton.IconOnly + Layout.preferredWidth: 22 + Layout.preferredHeight: 22 enabled: !root.loading onClicked: root.refresh() } @@ -210,7 +224,7 @@ PlasmoidItem { Rectangle { Layout.fillWidth: true Layout.preferredHeight: 1 - color: Qt.rgba(0, 188 / 255, 212 / 255, 0.45) + color: Qt.rgba(0, 188 / 255, 212 / 255, 0.35) } PC3.Label { @@ -218,124 +232,111 @@ PlasmoidItem { Layout.fillWidth: true wrapMode: Text.Wrap color: Kirigami.Theme.negativeTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 text: i18n("Errore: %1", root.errorMsg) } - LimitRow { - Layout.fillWidth: true - title: i18n("Sessione") - period: "5H" - remaining: root.usage ? root.usage.session.remaining : 0 - used: root.usage ? root.usage.session.used : 0 - accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.session : null), "#00bcd4") - models: (root.showModels && root.usage) ? root.usage.session.models : [] - } - + // Card 1: Settimanale (7D) LimitRow { Layout.fillWidth: true title: i18n("Settimanale") period: "7D" - remaining: root.usage ? root.usage.weekly.remaining : 0 - used: root.usage ? root.usage.weekly.used : 0 - accent: root.accentFor(root.pctRemaining(root.usage ? root.usage.weekly : null), "#7cb342") - models: (root.showModels && root.usage) ? root.usage.weekly.models : [] + remaining: root.usage && root.usage.weekly ? root.usage.weekly.remaining : 0 + used: root.usage && root.usage.weekly ? root.usage.weekly.used : 0 + accent: root.accentColorFor(root.secRemainingPct()) + models: (root.showModels && root.usage && root.usage.weekly) ? root.usage.weekly.models : [] + reset: root.usage && root.usage.resets ? root.usage.resets.weekly : null + clockTick: root.clockTick } - Rectangle { + // Card 2: Finestra 5 Ore (5H) + LimitRow { Layout.fillWidth: true - Layout.preferredHeight: 28 - radius: 5 - color: Qt.rgba(0, 0, 0, 0.28) - - GridLayout { - anchors.fill: parent - anchors.margins: 5 - columns: 2 - columnSpacing: 8 - rowSpacing: 0 - PC3.Label { - text: i18n("COSTO 4W") - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - PC3.Label { - text: i18n("MODELLO TOP") - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - PC3.Label { - text: root.usage && root.usage.activity ? "$" + (root.usage.activity.cost || "0.00") : "—" - font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 - } - PC3.Label { - text: root.usage ? root.shortModel(root.usage.weekly) : "—" - font.bold: true - elide: Text.ElideRight - Layout.maximumWidth: 94 - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 - } - } + title: i18n("Finestra 5 ore") + period: "5H" + remaining: root.usage && root.usage.session ? root.usage.session.remaining : 0 + used: root.usage && root.usage.session ? root.usage.session.used : 0 + accent: root.accentColorFor(root.primRemainingPct()) + models: (root.showModels && root.usage && root.usage.session) ? root.usage.session.models : [] + reset: root.usage && root.usage.resets ? root.usage.resets.session : null + clockTick: root.clockTick } + // Card 3: Costo 4W + Modello Top Rectangle { Layout.fillWidth: true Layout.preferredHeight: 40 radius: 5 - color: Qt.rgba(0, 0, 0, 0.28) + color: Qt.rgba(0, 0, 0, 0.25) - ColumnLayout { + RowLayout { anchors.fill: parent - anchors.margins: 4 - spacing: 0 - RowLayout { - Layout.fillWidth: true + anchors.leftMargin: 7 + anchors.rightMargin: 7 + anchors.topMargin: 4 + anchors.bottomMargin: 6 + spacing: 8 + + ColumnLayout { + spacing: 1 PC3.Label { - text: root.resetStatus() - color: "#00bcd4" - font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - Item { Layout.fillWidth: true } - PC3.Label { - text: root.lastUpdate ? i18n("SYNC %1", root.lastUpdate) : i18n("SYNC…") + text: i18n("COSTO 4W") color: Kirigami.Theme.disabledTextColor font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 } - } - RowLayout { - Layout.fillWidth: true PC3.Label { - text: i18n("5H %1", root.resetMoment(root.usage ? root.usage.resets.session : null)) - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 - } - Item { Layout.fillWidth: true } - PC3.Label { - text: root.resetRemaining(root.usage ? root.usage.resets.session : null) - color: "#00bcd4" + text: root.usage && root.usage.activity && root.usage.activity.cost !== undefined ? "$" + Number(root.usage.activity.cost).toFixed(2) : "$0.00" font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + color: root.brandColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 } } - RowLayout { - Layout.fillWidth: true + + Item { Layout.fillWidth: true } + + ColumnLayout { + spacing: 1 + Layout.alignment: Qt.AlignRight PC3.Label { - text: i18n("7D %1", root.resetMoment(root.usage ? root.usage.resets.weekly : null)) + text: i18n("MODELLO TOP") color: Kirigami.Theme.disabledTextColor + Layout.alignment: Qt.AlignRight font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 } - Item { Layout.fillWidth: true } PC3.Label { - text: root.resetRemaining(root.usage ? root.usage.resets.weekly : null) - color: "#7cb342" + text: root.usage && root.usage.weekly ? root.shortModel(root.usage.weekly) : "—" font.bold: true - font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + color: root.brandColor + elide: Text.ElideRight + Layout.maximumWidth: 100 + Layout.alignment: Qt.AlignRight + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 } } } } + + // Footer + RowLayout { + Layout.fillWidth: true + PC3.Label { + text: root.lastUpdate ? i18n("SYNC %1", root.lastUpdate) : i18n("SYNC…") + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + Item { Layout.fillWidth: true } + PC3.Label { + text: (root.secRemainingPct() === 0 || root.primRemainingPct() === 0) + ? i18n("✕ LIMITE ATTIVO") + : i18n("STATO ATTIVO") + color: (root.secRemainingPct() === 0 || root.primRemainingPct() === 0) + ? Kirigami.Theme.negativeTextColor + : Kirigami.Theme.disabledTextColor + font.bold: (root.secRemainingPct() === 0 || root.primRemainingPct() === 0) + font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 + } + } } } } -} \ No newline at end of file +}