feat: dual-window 5h/7d monitoring, reset credits, and visual harmonization

This commit is contained in:
enne2
2026-09-06 18:50:30 +02:00
parent f8a4742ac3
commit 75cf3e04b7
6 changed files with 309 additions and 206 deletions
+9 -29
View File
@@ -2,22 +2,18 @@
[![Gitea](https://img.shields.io/badge/Gitea-enne2%2Fkde--codex--dashboard-blue?logo=gitea)](https://git.enne2.net/enne2/kde-codex-dashboard) [![Gitea](https://img.shields.io/badge/Gitea-enne2%2Fkde--codex--dashboard-blue?logo=gitea)](https://git.enne2.net/enne2/kde-codex-dashboard)
Widget desktop 224×224 per KDE Plasma 6 che mostra il **residuo settimanale** Widget desktop 224×224 per KDE Plasma 6 che mostra il **residuo a doppia finestra** (Settimanale 7D e Finestra 5 ore) di [OpenAI Codex](https://openai.com/codex), autenticato via OAuth (profilo ChatGPT).
di [OpenAI Codex](https://openai.com/codex), autenticato via OAuth
(profilo ChatGPT).
Legge il token OAuth da `~/.pi/agent/auth.json` (provider `openai-codex` Legge il token OAuth da `~/.pi/agent/auth.json` (provider `openai-codex` configurato nel [pi agent](https://pi.dev)) e interroga l'endpoint `GET https://chatgpt.com/backend-api/wham/usage`.
configurato nel [pi agent](https://pi.dev)) e interroga l'endpoint
(non documentato) `GET https://chatgpt.com/backend-api/wham/usage`.
## Funzionalità ## Funzionalità
- **Piano**: Plus / Free / Team visibile nell'header. - **Piano**: Plus / Free / Team visibile nell'header.
- **Residuo settimanale** con barra di avanzamento e percentuale. - **Doppia Finestra Rate Limit**:
- **Countdown esatto** al reset (data/ora + giorni/ore/minuti mancanti), - **Finestra Settimanale (7D)** con barra di avanzamento e countdown esatto di reset.
direttamente dal campo `reset_at` della risposta API. - **Finestra 5 Ore (5H)** con barra di avanzamento e countdown esatto di reset.
- **Crediti**: saldo crediti extra (se disponibili). - **Crediti di Reset**: badge con conteggio dei crediti di reset gratuiti disponibili (`rate_limit_reset_credits`).
- **Stato limiti**: indicatore visivo se `limit_reached` o `spend_control.reached`. - **Stato limiti**: indicatore visivo di blocco attivo (`limit_reached` o `spend_control.reached`).
- **Aggiornamento automatico** configurabile + pulsante di refresh manuale. - **Aggiornamento automatico** configurabile + pulsante di refresh manuale.
## File ## File
@@ -25,28 +21,12 @@ configurato nel [pi agent](https://pi.dev)) e interroga l'endpoint
``` ```
metadata.json metadata del pacchetto (Plasma/Applet) metadata.json metadata del pacchetto (Plasma/Applet)
contents/config/main.xml schema della configurazione (kcfg) contents/config/main.xml schema della configurazione (kcfg)
contents/scripts/usage.sh backend: token OAuth, chiama wham/usage, stampa JSON contents/scripts/usage.sh backend: token OAuth, chiama wham/usage, stampa JSON
contents/ui/main.qml UI (dashboard 224×224) contents/ui/main.qml UI (dashboard 224×224)
contents/ui/configGeneral.qml pagina di configurazione contents/ui/configGeneral.qml pagina di configurazione
``` ```
## Installazione
```bash
# da sorgente
kpackagetool6 --type Plasma/Applet --install /home/enne2/Dev/org.enne2.codex.usage
# da archivio
kpackagetool6 --type Plasma/Applet --install dist/org.enne2.codex.usage.tar.gz
```
## Dipendenze ## Dipendenze
- `jq` e `curl` (per lo script di backend) - `jq` e `curl` (per lo script di backend)
- il token OAuth di OpenAI Codex in `~/.pi/agent/auth.json` sotto `openai-codex` (impostato da pi con `/login`) - il token OAuth di OpenAI Codex in `~/.pi/agent/auth.json` sotto `openai-codex` (impostato da pi con `/login`)
## Note
- L'endpoint `/backend-api/wham/usage` è **non documentato** e potrebbe cambiare.
- Se il token OAuth scade, il plasmoide mostrerà "token scaduto". Riesegui `/login`
in pi (provider `openai-codex`) per rigenerarlo.
+3 -3
View File
@@ -10,19 +10,19 @@
<min>30</min> <min>30</min>
<max>3600</max> <max>3600</max>
<label>Intervallo di aggiornamento (secondi)</label> <label>Intervallo di aggiornamento (secondi)</label>
<whatsthis>Quanto spesso il plasmoide aggiorna i dati Codex. Minimo 30s. Default 300s (5 minuti).</whatsthis> <whatsthis>Quanto spesso il plasmoide aggiorna le quote Codex. Minimo 30s. Default 300s (5 minuti).</whatsthis>
</entry> </entry>
<entry name="warnThreshold" type="Int"> <entry name="warnThreshold" type="Int">
<default>60</default> <default>60</default>
<min>0</min> <min>0</min>
<max>100</max> <max>100</max>
<label>Soglia gialla per il residuo (%)</label> <label>Soglia residuo giallo (%)</label>
</entry> </entry>
<entry name="critThreshold" type="Int"> <entry name="critThreshold" type="Int">
<default>30</default> <default>30</default>
<min>0</min> <min>0</min>
<max>100</max> <max>100</max>
<label>Soglia rossa per il residuo (%)</label> <label>Soglia residuo rosso (%)</label>
</entry> </entry>
</group> </group>
</kcfg> </kcfg>
+35 -12
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Backend del plasmoide OpenAI Codex Usage. # Backend del plasmoide OpenAI Codex Usage.
# Chiama l'endpoint (non documentato) GET https://chatgpt.com/backend-api/wham/usage # Chiama l'endpoint (non documentato) GET https://chatgpt.com/backend-api/wham/usage
# con OAuth Bearer. La risposta contiene reset_at esatto → nessuna stima locale. # con OAuth Bearer e chatgpt-account-id.
set -u set -u
AUTH_JSON="${CODEX_AUTH_JSON:-$HOME/.pi/agent/auth.json}" AUTH_JSON="${CODEX_AUTH_JSON:-$HOME/.pi/agent/auth.json}"
TOKEN="" TOKEN=""
ACCOUNT_ID=""
if [ -f "$AUTH_JSON" ]; then if [ -f "$AUTH_JSON" ]; then
TOKEN="$(jq -r '.["openai-codex"].access // empty' "$AUTH_JSON" 2>/dev/null)" TOKEN="$(jq -r '.["openai-codex"].access // empty' "$AUTH_JSON" 2>/dev/null)"
ACCOUNT_ID="$(jq -r '.["openai-codex"].accountId // empty' "$AUTH_JSON" 2>/dev/null)"
fi fi
[ -n "$TOKEN" ] || TOKEN="${CODEX_ACCESS_TOKEN:-}" [ -n "$TOKEN" ] || TOKEN="${CODEX_ACCESS_TOKEN:-}"
[ -n "$TOKEN" ] || TOKEN="${OPENAI_CODEX_ACCESS_TOKEN:-}" [ -n "$TOKEN" ] || TOKEN="${OPENAI_CODEX_ACCESS_TOKEN:-}"
@@ -17,29 +19,50 @@ if [ -z "$TOKEN" ]; then
exit 0 exit 0
fi fi
RESP="$(curl -sS --max-time 15 "https://chatgpt.com/backend-api/wham/usage" -H "Authorization: Bearer $TOKEN" 2>/dev/null)" REQ_HEADERS=(-H "Authorization: Bearer $TOKEN")
if [ -n "$ACCOUNT_ID" ]; then
REQ_HEADERS+=(-H "chatgpt-account-id: $ACCOUNT_ID")
fi
REQ_HEADERS+=(-H "User-Agent: pi-agent/1.0")
RESP="$(curl -sS --max-time 15 "${REQ_HEADERS[@]}" "https://chatgpt.com/backend-api/wham/usage" 2>/dev/null)"
CURL_RC=$? CURL_RC=$?
if [ "$CURL_RC" -ne 0 ] || [ -z "$RESP" ]; then if [ "$CURL_RC" -ne 0 ] || [ -z "$RESP" ]; then
printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}\n' "$CURL_RC" printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}\n' "$CURL_RC"
exit 0 exit 0
fi fi
# Se il token è scaduto, l'API restituisce HTML di login invece di JSON. # Se il token è scaduto o non valido, l'API non restituisce rate_limit.
if ! printf '%s' "$RESP" | jq -e '.rate_limit.primary_window.used_percent | type == "number"' >/dev/null 2>&1; then if ! printf '%s' "$RESP" | jq -e '.rate_limit.primary_window.used_percent | type == "number"' >/dev/null 2>&1; then
printf '{"ok":false,"error":"token_expired","detail":"token Codex scaduto, riesegui /login in pi"}\n' printf '{"ok":false,"error":"token_expired","detail":"token Codex scaduto o non valido, riesegui /login in pi"}\n'
exit 0 exit 0
fi fi
printf '%s' "$RESP" | jq -c '{ printf '%s' "$RESP" | jq -c '{
ok: true, ok: true,
plan_type: .plan_type, plan_type: (.plan_type // "—"),
remaining: ((100 - .rate_limit.primary_window.used_percent) / 100), allowed: (.rate_limit.allowed // true),
used_percent: .rate_limit.primary_window.used_percent, limit_reached: (.rate_limit.limit_reached // false),
reset_at: .rate_limit.primary_window.reset_at, primary_window: {
limit_window: .rate_limit.primary_window.limit_window_seconds, used_percent: (.rate_limit.primary_window.used_percent // 0),
credits: { balance: .credits.balance, has_credits: .credits.has_credits }, remaining: ((100 - (.rate_limit.primary_window.used_percent // 0)) / 100),
spend_reached: .spend_control.reached, reset_at: .rate_limit.primary_window.reset_at,
limit_reached: .rate_limit.limit_reached limit_window_seconds: (.rate_limit.primary_window.limit_window_seconds // 18000),
reset_after_seconds: .rate_limit.primary_window.reset_after_seconds
},
secondary_window: {
used_percent: (.rate_limit.secondary_window.used_percent // 0),
remaining: ((100 - (.rate_limit.secondary_window.used_percent // 0)) / 100),
reset_at: .rate_limit.secondary_window.reset_at,
limit_window_seconds: (.rate_limit.secondary_window.limit_window_seconds // 604800),
reset_after_seconds: .rate_limit.secondary_window.reset_after_seconds
},
reset_credits: (.rate_limit_reset_credits.available_count // 0),
credits: {
balance: (.credits.balance // "0"),
has_credits: (.credits.has_credits // false)
},
spend_reached: (.spend_control.reached // false)
}' 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da wham/usage"}\n' }' 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da wham/usage"}\n'
exit 0 exit 0
+254 -154
View File
@@ -11,7 +11,7 @@ import org.kde.kirigami as Kirigami
PlasmoidItem { PlasmoidItem {
id: root id: root
property var data: null // { ok, plan_type, remaining, used_percent, reset_at, limit_window, credits, spend_reached, limit_reached } property var data: null
property string lastUpdate: "" property string lastUpdate: ""
property string errorMsg: "" property string errorMsg: ""
property bool loading: false property bool loading: false
@@ -21,13 +21,17 @@ PlasmoidItem {
property int warnThreshold: Plasmoid.configuration.warnThreshold property int warnThreshold: Plasmoid.configuration.warnThreshold
property int critThreshold: Plasmoid.configuration.critThreshold property int critThreshold: Plasmoid.configuration.critThreshold
readonly property color brandColor: "#7cb342"
Plasmoid.title: i18n("OpenAI Codex") Plasmoid.title: i18n("OpenAI Codex")
Plasmoid.icon: "code-context" Plasmoid.icon: "code-context"
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
toolTipMainText: i18n("Codex — utilizzo settimanale") toolTipMainText: i18n("OpenAI Codex — Rate Limits")
toolTipSubText: root.data ? i18n("Residuo: %1% · %2", toolTipSubText: root.data ? i18n("Settimana (7D): %1% · Finestra 5h: %2% · Piano: %3 · Reset disp: %4",
Math.round((root.data.remaining || 0) * 100), root.secRemainingPct(),
root.data.plan_type || "—") : (root.errorMsg || "") root.primRemainingPct(),
(root.data.plan_type || "—").toUpperCase(),
root.data.reset_credits || 0) : (root.errorMsg || "")
preferredRepresentation: fullRepresentation preferredRepresentation: fullRepresentation
switchWidth: 150 switchWidth: 150
@@ -50,7 +54,7 @@ PlasmoidItem {
if (parsed && parsed.ok === false) { if (parsed && parsed.ok === false) {
root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "") root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "")
root.data = null root.data = null
} else if (parsed && parsed.ok && parsed.remaining !== undefined) { } else if (parsed && parsed.ok && parsed.primary_window && parsed.secondary_window) {
root.data = parsed root.data = parsed
root.errorMsg = "" root.errorMsg = ""
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss") root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
@@ -73,25 +77,41 @@ PlasmoidItem {
} }
function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" } function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" }
function remainingPct() { return root.data ? Math.round((root.data.remaining || 0) * 100) : 0 } function primRemainingPct() {
function usedPct() { return root.data ? Math.round(root.data.used_percent || 0) : 0 } return root.data && root.data.primary_window ? Math.round((root.data.primary_window.remaining || 0) * 100) : 0
function accentColor() { }
const pct = remainingPct() function primUsedPct() {
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor return root.data && root.data.primary_window ? Math.round(root.data.primary_window.used_percent || 0) : 0
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
return "#7cb342"
} }
function resetMoment() { function secRemainingPct() {
const tick = root.clockTick return root.data && root.data.secondary_window ? Math.round((root.data.secondary_window.remaining || 0) * 100) : 0
if (!root.data || !root.data.reset_at) return "—"
const d = new Date(root.data.reset_at * 1000)
return d.toLocaleDateString(Qt.locale(), "dd/MM") + " " + d.toLocaleTimeString(Qt.locale(), "HH:mm")
} }
function resetRemaining() { function secUsedPct() {
return root.data && root.data.secondary_window ? Math.round(root.data.secondary_window.used_percent || 0) : 0
}
function effectiveRemainingPct() {
if (!root.data) return 0
return Math.min(primRemainingPct(), secRemainingPct())
}
function accentColorFor(pct) {
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
return root.brandColor
}
function globalAccentColor() {
if (!root.data) return Kirigami.Theme.disabledTextColor
if (root.data.limit_reached || root.data.allowed === false) return Kirigami.Theme.negativeTextColor
return accentColorFor(effectiveRemainingPct())
}
function formatResetRemaining(resetAt) {
const tick = root.clockTick const tick = root.clockTick
if (!root.data || !root.data.reset_at) return "—" if (!resetAt) return "—"
let mins = Math.max(0, Math.floor((root.data.reset_at * 1000 - Date.now()) / 60000)) let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000))
if (mins === 0) return i18n("ora") if (mins === 0) return i18n("ora")
const days = Math.floor(mins / 1440) const days = Math.floor(mins / 1440)
mins -= days * 1440 mins -= days * 1440
@@ -100,14 +120,6 @@ PlasmoidItem {
return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins) return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins)
} }
function windowLabel() {
if (!root.data || !root.data.limit_window) return "7D"
const secs = root.data.limit_window
if (secs >= 604800) return "7D"
if (secs >= 3600) return Math.round(secs / 3600) + "H"
return Math.round(secs / 60) + "M"
}
Component.onCompleted: root.refresh() Component.onCompleted: root.refresh()
Timer { Timer {
interval: Math.max(30, root.refreshSecs) * 1000 interval: Math.max(30, root.refreshSecs) * 1000
@@ -129,7 +141,7 @@ PlasmoidItem {
Kirigami.Icon { Kirigami.Icon {
anchors.fill: parent anchors.fill: parent
source: Plasmoid.icon source: Plasmoid.icon
color: root.data ? root.accentColor() : Kirigami.Theme.disabledTextColor color: root.data ? root.globalAccentColor() : Kirigami.Theme.disabledTextColor
} }
MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded } MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded }
} }
@@ -144,42 +156,47 @@ PlasmoidItem {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 9 anchors.margins: 7
spacing: 5 spacing: 4
// header // Header
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 5
Kirigami.Icon { Kirigami.Icon {
source: Plasmoid.icon source: Plasmoid.icon
color: "#7cb342" color: root.brandColor
Layout.preferredWidth: 20 Layout.preferredWidth: 18
Layout.preferredHeight: 20 Layout.preferredHeight: 18
} }
ColumnLayout { ColumnLayout {
spacing: 0 spacing: 0
PC3.Label { PC3.Label {
text: i18n("CODEX") text: i18n("CODEX")
font.bold: true font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1 font.pointSize: Kirigami.Theme.defaultFont.pointSize
} }
PC3.Label { PC3.Label {
text: root.data ? (root.data.plan_type || "—").toUpperCase() : "—" text: root.data ? (root.data.plan_type || "—").toUpperCase() : i18n("QUOTA DASHBOARD")
color: "#7cb342" color: root.brandColor
font.bold: true font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2 font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
} }
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
Rectangle { Rectangle {
Layout.preferredWidth: 9 Layout.preferredWidth: 8
Layout.preferredHeight: 9 Layout.preferredHeight: 8
radius: 5 radius: 4
color: root.errorMsg ? Kirigami.Theme.negativeTextColor : "#7cb342" color: root.errorMsg ? Kirigami.Theme.negativeTextColor :
(root.data && (root.data.limit_reached || root.data.allowed === false) ? Kirigami.Theme.negativeTextColor : root.brandColor)
} }
PC3.Button { PC3.Button {
icon.name: "view-refresh" icon.name: "view-refresh"
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
Layout.preferredWidth: 22
Layout.preferredHeight: 22
enabled: !root.loading enabled: !root.loading
onClicked: root.refresh() onClicked: root.refresh()
} }
@@ -188,7 +205,7 @@ PlasmoidItem {
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 1 Layout.preferredHeight: 1
color: Qt.rgba(0.49, 0.76, 0.26, 0.45) color: Qt.rgba(0.49, 0.76, 0.26, 0.35)
} }
PC3.Label { PC3.Label {
@@ -196,95 +213,18 @@ PlasmoidItem {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap wrapMode: Text.Wrap
color: Kirigami.Theme.negativeTextColor color: Kirigami.Theme.negativeTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
text: i18n("Errore: %1", root.errorMsg) text: i18n("Errore: %1", root.errorMsg)
} }
// barra principale // Card 1: Settimanale (7D)
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 72 Layout.preferredHeight: 52
radius: 6
color: Qt.rgba(0, 0, 0, 0.26)
border.width: 1
border.color: root.accentColor()
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
spacing: 3
RowLayout {
Layout.fillWidth: true
spacing: 5
Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 15
radius: 8
color: root.accentColor()
PC3.Label {
anchors.centerIn: parent
text: root.windowLabel()
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
}
}
PC3.Label {
text: i18n("Settimanale")
font.bold: true
Layout.fillWidth: true
}
PC3.Label {
text: root.remainingPct() + "%"
color: root.accentColor()
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 8
Rectangle {
anchors.fill: parent
radius: 4
color: Qt.rgba(0, 0, 0, 0.55)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, root.data ? root.data.remaining : 0))
height: parent.height
radius: 4
color: root.accentColor()
}
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: i18n("%1% usato", root.usedPct())
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
visible: Boolean(root.data && root.data.limit_reached)
text: i18n("✕ LIMITE")
color: Kirigami.Theme.negativeTextColor
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
// reset + crediti
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 54
radius: 5 radius: 5
color: Qt.rgba(0, 0, 0, 0.28) color: Qt.rgba(0, 0, 0, 0.25)
border.width: 1
border.color: root.accentColorFor(root.secRemainingPct())
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -293,40 +233,63 @@ PlasmoidItem {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
PC3.Label { spacing: 4
text: i18n("RESET") Rectangle {
color: Kirigami.Theme.disabledTextColor Layout.preferredWidth: 22
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 Layout.preferredHeight: 14
radius: 7
color: root.accentColorFor(root.secRemainingPct())
PC3.Label {
anchors.centerIn: parent
text: "7D"
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
} }
Item { Layout.fillWidth: true }
PC3.Label { PC3.Label {
text: root.resetMoment() text: i18n("Settimanale")
color: root.accentColor()
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
}
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: root.resetRemaining()
color: "#7cb342"
font.bold: true font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1 font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
Layout.fillWidth: true
} }
Item { Layout.fillWidth: true }
PC3.Label { PC3.Label {
text: root.data && root.data.credits ? i18n("Cred. %1", root.data.credits.balance || "0") : "" text: root.secRemainingPct() + "%"
color: Kirigami.Theme.disabledTextColor color: root.accentColorFor(root.secRemainingPct())
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
} }
} }
Item {
Layout.fillWidth: true
Layout.preferredHeight: 6
Rectangle {
anchors.fill: parent
radius: 3
color: Qt.rgba(0, 0, 0, 0.55)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, root.data && root.data.secondary_window ? root.data.secondary_window.remaining : 0))
height: parent.height
radius: 3
color: root.accentColorFor(root.secRemainingPct())
}
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
PC3.Label { PC3.Label {
visible: Boolean(root.data && root.data.spend_reached) text: i18n("%1% usato", root.secUsedPct())
text: i18n("Spend control raggiunto") color: Kirigami.Theme.disabledTextColor
color: Kirigami.Theme.negativeTextColor font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("Reset: %1", root.formatResetRemaining(root.data && root.data.secondary_window ? root.data.secondary_window.reset_at : null))
color: root.accentColorFor(root.secRemainingPct())
font.bold: true font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
} }
@@ -334,6 +297,139 @@ PlasmoidItem {
} }
} }
// Card 2: Finestra 5 Ore (5H)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
border.width: 1
border.color: root.accentColorFor(root.primRemainingPct())
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 4
Rectangle {
Layout.preferredWidth: 22
Layout.preferredHeight: 14
radius: 7
color: root.accentColorFor(root.primRemainingPct())
PC3.Label {
anchors.centerIn: parent
text: "5H"
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
PC3.Label {
text: i18n("Finestra 5 ore")
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
Layout.fillWidth: true
}
PC3.Label {
text: root.primRemainingPct() + "%"
color: root.accentColorFor(root.primRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 6
Rectangle {
anchors.fill: parent
radius: 3
color: Qt.rgba(0, 0, 0, 0.55)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, root.data && root.data.primary_window ? root.data.primary_window.remaining : 0))
height: parent.height
radius: 3
color: root.accentColorFor(root.primRemainingPct())
}
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: i18n("%1% usato", root.primUsedPct())
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("Reset: %1", root.formatResetRemaining(root.data && root.data.primary_window ? root.data.primary_window.reset_at : null))
color: root.accentColorFor(root.primRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
// Card 3: Crediti Extra + Reset Disponibili
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 7
anchors.rightMargin: 7
anchors.topMargin: 4
anchors.bottomMargin: 6
spacing: 8
ColumnLayout {
spacing: 1
PC3.Label {
text: i18n("CREDITI EXTRA")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: root.data && root.data.credits && root.data.credits.balance ? "$" + root.data.credits.balance : "$0.00"
font.bold: true
color: root.brandColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
}
}
Item { Layout.fillWidth: true }
ColumnLayout {
spacing: 1
Layout.alignment: Qt.AlignRight
PC3.Label {
text: i18n("RESET SCORTA")
color: Kirigami.Theme.disabledTextColor
Layout.alignment: Qt.AlignRight
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: (root.data && root.data.reset_credits !== undefined) ? i18n("↺ %1 disp.", root.data.reset_credits) : "—"
font.bold: true
color: "#38bdf8"
Layout.alignment: Qt.AlignRight
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
}
}
}
}
// Footer
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
PC3.Label { PC3.Label {
@@ -343,9 +439,13 @@ PlasmoidItem {
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
PC3.Label { PC3.Label {
text: root.data && root.data.plan_type ? root.data.plan_type.toUpperCase() : "" text: root.data && (root.data.limit_reached || root.data.allowed === false)
color: Kirigami.Theme.disabledTextColor ? i18n("✕ LIMITE ATTIVO")
font.bold: true : i18n("STATO ATTIVO")
color: root.data && (root.data.limit_reached || root.data.allowed === false)
? Kirigami.Theme.negativeTextColor
: Kirigami.Theme.disabledTextColor
font.bold: Boolean(root.data && (root.data.limit_reached || root.data.allowed === false))
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3 font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
} }
} }
+3 -3
View File
@@ -4,12 +4,12 @@
"Id": "org.enne2.codex.usage", "Id": "org.enne2.codex.usage",
"Name": "OpenAI Codex Usage", "Name": "OpenAI Codex Usage",
"Name[it]": "Utilizzo OpenAI Codex", "Name[it]": "Utilizzo OpenAI Codex",
"Description": "Shows the weekly usage allowance for OpenAI Codex (ChatGPT OAuth profile), with exact reset countdown, plan type, and credits", "Description": "Shows dual-window (5h and 7d) usage allowance for OpenAI Codex (ChatGPT OAuth profile), with exact reset countdown, plan type, and banked reset credits",
"Description[it]": "Mostra il residuo settimanale di OpenAI Codex (profilo ChatGPT OAuth), con countdown esatto del reset, tipo piano e crediti", "Description[it]": "Mostra il residuo a doppia finestra (5h e 7d) di OpenAI Codex (profilo ChatGPT OAuth), con countdown esatto del reset, tipo piano e crediti di reset",
"Icon": "code-context", "Icon": "code-context",
"Category": "System Information", "Category": "System Information",
"License": "GPL-3.0", "License": "GPL-3.0",
"Version": "1.0.0", "Version": "1.1.0",
"Authors": [ { "Name": "enne2" } ] "Authors": [ { "Name": "enne2" } ]
}, },
"X-Plasma-API-Minimum-Version": "6.0" "X-Plasma-API-Minimum-Version": "6.0"