feat: initial commit for Antigravity Usage Plasmoid (Plasma 6)
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
/* Antigravity Usage — dashboard desktop per KDE Plasma 6. */
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls as QQC2
|
||||
import org.kde.plasma.plasmoid
|
||||
import org.kde.plasma.core as PlasmaCore
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.plasma.plasma5support as Plasma5Support
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
PlasmoidItem {
|
||||
id: root
|
||||
|
||||
property var data: null // { ok, project, groups: [ { id, name, models, remaining, used_percent, reset_time, reset_at } ] }
|
||||
property string lastUpdate: ""
|
||||
property string errorMsg: ""
|
||||
property bool loading: false
|
||||
property int clockTick: 0
|
||||
|
||||
property int refreshSecs: Plasmoid.configuration.refreshIntervalSec
|
||||
property int warnThreshold: Plasmoid.configuration.warnThreshold
|
||||
property int critThreshold: Plasmoid.configuration.critThreshold
|
||||
|
||||
readonly property color themeViolet: "#a855f7"
|
||||
readonly property color themeIndigo: "#818cf8"
|
||||
readonly property color themeCyan: "#38bdf8"
|
||||
readonly property color themeGreen: "#4ade80"
|
||||
|
||||
Plasmoid.title: i18n("Antigravity")
|
||||
Plasmoid.icon: "compass"
|
||||
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
|
||||
toolTipMainText: i18n("Antigravity — quote modelli")
|
||||
toolTipSubText: root.data && root.data.groups && root.data.groups.length > 0
|
||||
? i18n("Gemini: %1% · Claude: %2%",
|
||||
Math.round((root.data.groups[0].remaining || 0) * 100),
|
||||
Math.round((root.data.groups.length > 1 ? root.data.groups[1].remaining || 0 : 0) * 100))
|
||||
: (root.errorMsg || "")
|
||||
|
||||
preferredRepresentation: fullRepresentation
|
||||
switchWidth: 150
|
||||
switchHeight: 150
|
||||
|
||||
Plasma5Support.DataSource {
|
||||
id: executable
|
||||
engine: "executable"
|
||||
connectedSources: []
|
||||
|
||||
onNewData: function(sourceName, result) {
|
||||
root.loading = false
|
||||
executable.disconnectSource(sourceName)
|
||||
if (result["exit code"] !== 0) {
|
||||
root.errorMsg = i18n("comando fallito (exit %1)", result["exit code"])
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse((result["stdout"] || "").trim())
|
||||
if (parsed && parsed.ok === false) {
|
||||
root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "")
|
||||
root.data = null
|
||||
} else if (parsed && parsed.ok && parsed.groups) {
|
||||
root.data = parsed
|
||||
root.errorMsg = ""
|
||||
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
||||
} else {
|
||||
root.errorMsg = i18n("risposta inattesa")
|
||||
root.data = null
|
||||
}
|
||||
} catch (e) {
|
||||
root.errorMsg = i18n("JSON non valido: %1", e.message)
|
||||
root.data = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (root.loading) return
|
||||
root.loading = true
|
||||
const path = Qt.resolvedUrl("../scripts/usage.sh").toString().replace(/^file:\/\//, "")
|
||||
executable.connectSource("/bin/bash " + shellQuote(path))
|
||||
}
|
||||
|
||||
function shellQuote(s) {
|
||||
return "'" + String(s).replace(/'/g, "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
function accentColorFor(pct) {
|
||||
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor
|
||||
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
|
||||
return root.themeIndigo
|
||||
}
|
||||
|
||||
function countdownFor(resetAt) {
|
||||
const tick = root.clockTick
|
||||
if (!resetAt || resetAt <= 0) 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)
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
|
||||
Timer {
|
||||
interval: Math.max(30, root.refreshSecs) * 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 30000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.clockTick += 1
|
||||
}
|
||||
|
||||
compactRepresentation: Component {
|
||||
Item {
|
||||
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||
Kirigami.Icon {
|
||||
anchors.fill: parent
|
||||
source: Plasmoid.icon
|
||||
color: root.data ? root.themeIndigo : Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: Plasmoid.expanded = !Plasmoid.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fullRepresentation: Component {
|
||||
Item {
|
||||
implicitWidth: 224
|
||||
implicitHeight: 224
|
||||
Layout.preferredWidth: 224
|
||||
Layout.preferredHeight: 224
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 4
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Kirigami.Icon {
|
||||
source: Plasmoid.icon
|
||||
color: root.themeViolet
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
PC3.Label {
|
||||
text: i18n("ANTIGRAVITY")
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
|
||||
}
|
||||
PC3.Label {
|
||||
text: root.data && root.data.project ? "CLOUDCODE-PA" : "GOOGLE AI"
|
||||
color: root.themeViolet
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 8
|
||||
Layout.preferredHeight: 8
|
||||
radius: 4
|
||||
color: root.errorMsg ? Kirigami.Theme.negativeTextColor : root.themeGreen
|
||||
}
|
||||
|
||||
PC3.Button {
|
||||
icon.name: "view-refresh"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
enabled: !root.loading
|
||||
onClicked: root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Separatore
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Qt.rgba(0.66, 0.33, 0.97, 0.35)
|
||||
}
|
||||
|
||||
// Visualizzazione Errore
|
||||
PC3.Label {
|
||||
visible: root.errorMsg.length > 0
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Kirigami.Theme.negativeTextColor
|
||||
text: i18n("Errore: %1", root.errorMsg)
|
||||
}
|
||||
|
||||
// Lista delle 3 quote (Gemini, Claude, GPT-OSS)
|
||||
Repeater {
|
||||
model: root.data && root.data.groups ? root.data.groups : []
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 46
|
||||
radius: 5
|
||||
color: Qt.rgba(0, 0, 0, 0.28)
|
||||
border.width: 1
|
||||
border.color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100))
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
spacing: 2
|
||||
|
||||
// Riga 1: Nome gruppo + Modelli + % Residuo
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
PC3.Label {
|
||||
text: modelData.name || ""
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: "· " + (modelData.models || "")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: Math.round((modelData.remaining || 0) * 100) + "%"
|
||||
color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100))
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize
|
||||
}
|
||||
}
|
||||
|
||||
// Riga 2: Barra di avanzamento
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 5
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 2.5
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width * Math.max(0, Math.min(1, modelData.remaining || 0))
|
||||
height: parent.height
|
||||
radius: 2.5
|
||||
color: root.accentColorFor(Math.round((modelData.remaining || 0) * 100))
|
||||
}
|
||||
}
|
||||
|
||||
// Riga 3: Dettaglio usato + Reset countdown
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("%1% usato", modelData.used_percent !== undefined ? modelData.used_percent : 0)
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("reset %1", root.countdownFor(modelData.reset_at))
|
||||
color: root.themeIndigo
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// 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: "DAILY"
|
||||
color: root.themeViolet
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user