feat: initial commit for Antigravity Usage Plasmoid (Plasma 6)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
dist/
|
||||
*.tar.gz
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,38 @@
|
||||
# Antigravity Usage Plasmoid (`org.enne2.antigravity.usage`)
|
||||
|
||||
Widget desktop per **KDE Plasma 6** che visualizza in tempo reale le quote di utilizzo e i countdown di reset dei modelli disponibili tramite Google CloudCode PA (Antigravity):
|
||||
|
||||
- **Gemini 3.x** (Gemini 3.1 Pro, Gemini 3.5/3.6/3.7 Flash)
|
||||
- **Claude 4.6** (Claude Sonnet 4.6, Claude Opus 4.6 Thinking)
|
||||
- **GPT-OSS** (GPT-OSS 120B Medium)
|
||||
|
||||
---
|
||||
|
||||
## Caratteristiche
|
||||
|
||||
- **3 card di monitoraggio**: barra percentuale residua, percentuale usata e countdown preciso al reset della quota.
|
||||
- **Autenticazione OAuth e Refresh automatico**: legge il token da `~/.gemini/antigravity-cli/antigravity-oauth-token` ed esegue il refresh OAuth se necessario senza interruzione.
|
||||
- **Colori adattivi**: soglie configurabili per avviso (giallo) e critico (rosso), con accento cyberpunk indaco/viola.
|
||||
- **Formato Desktop**: progettato per la griglia desktop (224×224 px), integrabile a fianco dei widget OpenAI Codex e Ollama Cloud.
|
||||
|
||||
---
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
# Packaging
|
||||
tar -czvf dist/org.enne2.antigravity.usage.tar.gz -C . \
|
||||
metadata.json contents/
|
||||
|
||||
# Installazione Plasma 6
|
||||
kpackagetool6 -t Plasma/Applet --install dist/org.enne2.antigravity.usage.tar.gz
|
||||
|
||||
# Oppure aggiornamento
|
||||
kpackagetool6 -t Plasma/Applet --upgrade dist/org.enne2.antigravity.usage.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Licenza
|
||||
|
||||
GPL-3.0
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
|
||||
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||
<kcfgfile name=""/>
|
||||
<group name="General">
|
||||
<entry name="refreshIntervalSec" type="Int">
|
||||
<default>300</default>
|
||||
<min>30</min>
|
||||
<max>3600</max>
|
||||
<label>Intervallo di aggiornamento (secondi)</label>
|
||||
<whatsthis>Quanto spesso il plasmoide aggiorna le quote Antigravity. Minimo 30s. Default 300s (5 minuti).</whatsthis>
|
||||
</entry>
|
||||
<entry name="warnThreshold" type="Int">
|
||||
<default>60</default>
|
||||
<min>0</min>
|
||||
<max>100</max>
|
||||
<label>Soglia residuo giallo (%)</label>
|
||||
</entry>
|
||||
<entry name="critThreshold" type="Int">
|
||||
<default>30</default>
|
||||
<min>0</min>
|
||||
<max>100</max>
|
||||
<label>Soglia residuo rosso (%)</label>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Backend per il plasmoide KDE Plasma 6: Antigravity Usage Dashboard.
|
||||
|
||||
Interroga il gateway Google CloudCode PA (https://daily-cloudcode-pa.googleapis.com)
|
||||
leggendo il token OAuth da ~/.gemini/antigravity-cli/antigravity-oauth-token
|
||||
ed eseguendo il refresh automatico se scaduto o non valido.
|
||||
Restituisce lo stato delle quote per le 3 famiglie di modelli:
|
||||
- Gemini (Gemini 3.x Flash / Pro)
|
||||
- Claude (Claude Sonnet 4.6 / Opus 4.6)
|
||||
- GPT-OSS (GPT-OSS 120B)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
|
||||
TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token")
|
||||
OAUTH_URL = "https://oauth2.googleapis.com/token"
|
||||
BASE_URL = "https://daily-cloudcode-pa.googleapis.com"
|
||||
CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
||||
CLIENT_SECRET = "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"
|
||||
UA = "antigravity/cli/1.1.17"
|
||||
API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"
|
||||
METADATA = json.dumps({"ideType": "ANTIGRAVITY", "platform": "LINUX", "pluginType": "GEMINI"})
|
||||
|
||||
|
||||
def get_token_data():
|
||||
if not os.path.exists(TOKEN_FILE):
|
||||
return None
|
||||
try:
|
||||
with open(TOKEN_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def save_token_data(data):
|
||||
try:
|
||||
tmp = TOKEN_FILE + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
os.replace(tmp, TOKEN_FILE)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def refresh_token(refresh_tok):
|
||||
req_body = json.dumps({
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": CLIENT_SECRET,
|
||||
"refresh_token": refresh_tok,
|
||||
"grant_type": "refresh_token"
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(OAUTH_URL, data=req_body, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
res = json.loads(resp.read().decode("utf-8"))
|
||||
return res.get("access_token"), int(res.get("expires_in", 3600))
|
||||
|
||||
|
||||
def call_api(path, token, payload):
|
||||
url = f"{BASE_URL}{path}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA,
|
||||
"X-Goog-Api-Client": API_CLIENT,
|
||||
"Client-Metadata": METADATA
|
||||
}
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=body, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def parse_iso_epoch(ts):
|
||||
if not ts:
|
||||
return 0
|
||||
try:
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
return int(dt.timestamp())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
data = get_token_data()
|
||||
if not data or "token" not in data:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"error": "no_token",
|
||||
"detail": f"Nessun token OAuth in {TOKEN_FILE}"
|
||||
}))
|
||||
sys.exit(0)
|
||||
|
||||
tok = data["token"]
|
||||
access_tok = tok.get("access_token")
|
||||
refresh_tok = tok.get("refresh_token")
|
||||
|
||||
project_id = "healthy-topic-dwr0v"
|
||||
try:
|
||||
quota_data = call_api("/v1internal:retrieveUserQuota", access_tok, {"project": project_id})
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (401, 403) and refresh_tok:
|
||||
try:
|
||||
new_acc, exp_in = refresh_token(refresh_tok)
|
||||
tok["access_token"] = new_acc
|
||||
save_token_data(data)
|
||||
access_tok = new_acc
|
||||
quota_data = call_api("/v1internal:retrieveUserQuota", access_tok, {"project": project_id})
|
||||
except Exception as e2:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"error": "auth_error",
|
||||
"detail": f"Refresh fallito: {e2}"
|
||||
}))
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"error": f"http_{e.code}",
|
||||
"detail": f"Errore server Google HTTP {e.code}"
|
||||
}))
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(json.dumps({
|
||||
"ok": False,
|
||||
"error": "network_error",
|
||||
"detail": str(e)
|
||||
}))
|
||||
sys.exit(0)
|
||||
|
||||
buckets = quota_data.get("buckets", [])
|
||||
bucket_map = {b.get("modelId"): b for b in buckets}
|
||||
|
||||
# 1. Gemini Family
|
||||
gem_b = (bucket_map.get("gemini-3.1-pro-high") or
|
||||
bucket_map.get("gemini-3.7-flash-high") or
|
||||
bucket_map.get("gemini-3.5-flash-low") or {})
|
||||
gem_rem = float(gem_b.get("remainingFraction", 1.0))
|
||||
gem_reset_str = gem_b.get("resetTime", "")
|
||||
gemini_info = {
|
||||
"id": "gemini",
|
||||
"name": "Gemini 3.x",
|
||||
"models": "Pro / Flash",
|
||||
"remaining": round(gem_rem, 4),
|
||||
"used_percent": round((1.0 - gem_rem) * 100, 1),
|
||||
"reset_time": gem_reset_str,
|
||||
"reset_at": parse_iso_epoch(gem_reset_str)
|
||||
}
|
||||
|
||||
# 2. Claude Family
|
||||
cld_b = (bucket_map.get("claude-sonnet-4-6") or
|
||||
bucket_map.get("claude-opus-4-6-thinking") or {})
|
||||
cld_rem = float(cld_b.get("remainingFraction", 1.0))
|
||||
cld_reset_str = cld_b.get("resetTime", "")
|
||||
claude_info = {
|
||||
"id": "claude",
|
||||
"name": "Claude 4.6",
|
||||
"models": "Sonnet / Opus",
|
||||
"remaining": round(cld_rem, 4),
|
||||
"used_percent": round((1.0 - cld_rem) * 100, 1),
|
||||
"reset_time": cld_reset_str,
|
||||
"reset_at": parse_iso_epoch(cld_reset_str)
|
||||
}
|
||||
|
||||
# 3. GPT-OSS Family
|
||||
gpt_b = bucket_map.get("gpt-oss-120b-medium") or {}
|
||||
gpt_rem = float(gpt_b.get("remainingFraction", 1.0))
|
||||
gpt_reset_str = gpt_b.get("resetTime", "")
|
||||
gpt_info = {
|
||||
"id": "gpt-oss",
|
||||
"name": "GPT-OSS",
|
||||
"models": "120B Medium",
|
||||
"remaining": round(gpt_rem, 4),
|
||||
"used_percent": round((1.0 - gpt_rem) * 100, 1),
|
||||
"reset_time": gpt_reset_str,
|
||||
"reset_at": parse_iso_epoch(gpt_reset_str)
|
||||
}
|
||||
|
||||
out = {
|
||||
"ok": True,
|
||||
"project": project_id,
|
||||
"groups": [gemini_info, claude_info, gpt_info]
|
||||
}
|
||||
print(json.dumps(out, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* configGeneral.qml — pagina di configurazione del plasmoide Antigravity Usage.
|
||||
*/
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Item {
|
||||
id: page
|
||||
|
||||
property alias cfg_refreshIntervalSec: refreshSpin.value
|
||||
property alias cfg_warnThreshold: warnSpin.value
|
||||
property alias cfg_critThreshold: critSpin.value
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.largeSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("Intervallo di aggiornamento (secondi)")
|
||||
font.bold: true
|
||||
}
|
||||
PC3.SpinBox {
|
||||
id: refreshSpin
|
||||
from: 30
|
||||
to: 3600
|
||||
stepSize: 30
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("Soglia residuo giallo (%)")
|
||||
font.bold: true
|
||||
}
|
||||
PC3.SpinBox {
|
||||
id: warnSpin
|
||||
from: 0
|
||||
to: 100
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("Soglia residuo rosso (%)")
|
||||
font.bold: true
|
||||
}
|
||||
PC3.SpinBox {
|
||||
id: critSpin
|
||||
from: 0
|
||||
to: 100
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/Applet",
|
||||
"KPlugin": {
|
||||
"Id": "org.enne2.antigravity.usage",
|
||||
"Name": "Antigravity Usage",
|
||||
"Name[it]": "Utilizzo Antigravity",
|
||||
"Description": "Shows the usage quotas and reset countdowns for Antigravity models (Gemini 3.x, Claude 4.6, GPT-OSS 120B)",
|
||||
"Description[it]": "Mostra le quote di utilizzo e i countdown di reset per i modelli Antigravity (Gemini 3.x, Claude 4.6, GPT-OSS 120B)",
|
||||
"Icon": "compass",
|
||||
"Category": "System Information",
|
||||
"License": "GPL-3.0",
|
||||
"Version": "1.0.0",
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "enne2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"X-Plasma-API-Minimum-Version": "6.0"
|
||||
}
|
||||
Reference in New Issue
Block a user