feat: plasmoide KDE Plasma 6 per il credito residuo DeepSeek e OpenRouter
This commit is contained in:
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,43 @@
|
||||
# AI Credits Balance
|
||||
|
||||
Plasmoide (widget) per KDE Plasma 6 che mostra il **credito residuo** di **DeepSeek** e **OpenRouter**, leggendo le chiavi API da `~/.pi/agent/auth.json` (i provider `deepseek` e `openrouter` del pi agent).
|
||||
|
||||

|
||||
|
||||
## Dati mostrati
|
||||
|
||||
- **DeepSeek** — saldo totale (`total_balance`), scomposto in `granted` + `top-up`, con badge di disponibilità (`is_available`). Barra opzionale calcolata su un budget di riferimento configurabile.
|
||||
- **OpenRouter** — credito residuo (`total_credits - total_usage`), usato / totale e percentuale residua con barra.
|
||||
|
||||
## Sorgenti API
|
||||
|
||||
| Servizio | Endpoint | Autenticazione |
|
||||
|-------------|-------------------------------------------|--------------------------|
|
||||
| DeepSeek | `GET https://api.deepseek.com/user/balance` | `Authorization: Bearer <key>` |
|
||||
| OpenRouter | `GET https://openrouter.ai/api/v1/credits` | `Authorization: Bearer <key>` (management key) |
|
||||
|
||||
> **Nota OpenRouter:** l'endpoint `/api/v1/credits` richiede una *management key*. Una routing key normale può restituire un errore di autorizzazione.
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
# copia in ~/.local/share/plasma/plasmoids/ (riavvio plasmashell)
|
||||
cp -r org.enne2.ai-credits.balance ~/.local/share/plasmoids/
|
||||
systemctl --user restart plasma-plasmashell.service
|
||||
# poi "Aggiungi widget" → "Credito AI Residuo"
|
||||
```
|
||||
|
||||
Oppure via `kpackagetool6`:
|
||||
|
||||
```bash
|
||||
kpackagetool6 -t Plasma/Applet -i org.enne2.ai-credits.balance.tar.gz
|
||||
```
|
||||
|
||||
## Configurazione
|
||||
|
||||
Dal plasmoide → **Impostazioni**:
|
||||
|
||||
- Intervallo di aggiornamento (30–3600 s, default 300)
|
||||
- Soglia gialla/rossa residuo (%) per le barre
|
||||
- Budget di riferimento DeepSeek ($) per la barra (0 = nessuna barra)
|
||||
- Mostra/nascondi DeepSeek, OpenRouter
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
@@ -0,0 +1,45 @@
|
||||
<?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 i crediti. Minimo 30s. Default 300s (5 minuti).</whatsthis>
|
||||
</entry>
|
||||
<entry name="warnThreshold" type="Int">
|
||||
<default>30</default>
|
||||
<min>0</min>
|
||||
<max>100</max>
|
||||
<label>Soglia gialla per il residuo (%)</label>
|
||||
<whatsthis>Sotto questa percentuale di residuo la barra diventa gialla. Si applica alla barra di OpenRouter e (se dsBudget>0) a DeepSeek.</whatsthis>
|
||||
</entry>
|
||||
<entry name="critThreshold" type="Int">
|
||||
<default>15</default>
|
||||
<min>0</min>
|
||||
<max>100</max>
|
||||
<label>Soglia rossa per il residuo (%)</label>
|
||||
<whatsthis>Sotto questa percentuale di residuo la barra diventa rossa.</whatsthis>
|
||||
</entry>
|
||||
<entry name="dsBudget" type="Double">
|
||||
<default>10.0</default>
|
||||
<min>0</min>
|
||||
<max>100000</max>
|
||||
<label>Budget di riferimento DeepSeek ($)</label>
|
||||
<whatsthis>Il budget iniziale DeepSeek usato per calcolare la percentuale residua e la barra. Imposta a quanto hai caricato inizialmente. 0 = nasconde la barra (mostra solo il valore).</whatsthis>
|
||||
</entry>
|
||||
<entry name="showDeepSeek" type="Bool">
|
||||
<default>true</default>
|
||||
<label>Mostra DeepSeek</label>
|
||||
</entry>
|
||||
<entry name="showOpenRouter" type="Bool">
|
||||
<default>true</default>
|
||||
<label>Mostra OpenRouter</label>
|
||||
</entry>
|
||||
</group>
|
||||
</kcfg>
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backend del plasmoide AI Credits Balance.
|
||||
# Chiama le API di DeepSeek (GET /user/balance) e OpenRouter
|
||||
# (GET /api/v1/credits) e restituisce un JSON unificato. Gli errori sono
|
||||
# gestiti per-provider: anche se uno fallisce, l'altro viene comunque riportato.
|
||||
set -u
|
||||
|
||||
AUTH_JSON="${AI_AUTH_JSON:-$HOME/.pi/agent/auth.json}"
|
||||
NOW="$(date +%s)"
|
||||
|
||||
DS_KEY=""
|
||||
OR_KEY=""
|
||||
if [ -f "$AUTH_JSON" ]; then
|
||||
DS_KEY="$(jq -r '.["deepseek"].key // empty' "$AUTH_JSON" 2>/dev/null)"
|
||||
OR_KEY="$(jq -r '.["openrouter"].key // empty' "$AUTH_JSON" 2>/dev/null)"
|
||||
fi
|
||||
[ -n "$DS_KEY" ] || DS_KEY="${DEEPSEEK_API_KEY:-}"
|
||||
[ -n "$OR_KEY" ] || OR_KEY="${OPENROUTER_API_KEY:-}"
|
||||
|
||||
# --- DeepSeek -------------------------------------------------------------
|
||||
ds_block() {
|
||||
if [ -z "$DS_KEY" ]; then
|
||||
printf '{"ok":false,"error":"no_api_key"}'
|
||||
return
|
||||
fi
|
||||
local resp rc
|
||||
resp="$(curl -sS --max-time 15 https://api.deepseek.com/user/balance \
|
||||
-H "Authorization: Bearer $DS_KEY" 2>/dev/null)"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ] || [ -z "$resp" ]; then
|
||||
printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}' "$rc"
|
||||
return
|
||||
fi
|
||||
if ! printf '%s' "$resp" | jq -e '.balance_infos | type == "array"' >/dev/null 2>&1; then
|
||||
printf '{"ok":false,"error":"bad_json","detail":"risposta non valida da DeepSeek"}'
|
||||
return
|
||||
fi
|
||||
printf '%s' "$resp" | jq -c '{
|
||||
ok:true,
|
||||
available:(.is_available // false),
|
||||
currency:((.balance_infos[0].currency) // "USD"),
|
||||
total:((.balance_infos[0].total_balance) | tonumber),
|
||||
granted:((.balance_infos[0].granted_balance) | tonumber),
|
||||
topped_up:((.balance_infos[0].topped_up_balance) | tonumber)
|
||||
}' 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"impossibile elaborare DeepSeek"}'
|
||||
}
|
||||
|
||||
# --- OpenRouter -----------------------------------------------------------
|
||||
or_block() {
|
||||
if [ -z "$OR_KEY" ]; then
|
||||
printf '{"ok":false,"error":"no_api_key"}'
|
||||
return
|
||||
fi
|
||||
local resp rc
|
||||
resp="$(curl -sS --max-time 15 https://openrouter.ai/api/v1/credits \
|
||||
-H "Authorization: Bearer $OR_KEY" 2>/dev/null)"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ] || [ -z "$resp" ]; then
|
||||
printf '{"ok":false,"error":"fetch_failed","detail":"curl exit %s"}' "$rc"
|
||||
return
|
||||
fi
|
||||
if ! printf '%s' "$resp" | jq -e '.data.total_credits | type == "number"' >/dev/null 2>&1; then
|
||||
# 401/403 => la routing key non ha permessi sul /credits (serve management key).
|
||||
printf '{"ok":false,"error":"bad_json","detail":"risposta non valida (la chiave potrebbe essere una routing key: serve una management key)"}'
|
||||
return
|
||||
fi
|
||||
printf '%s' "$resp" | jq -c '{
|
||||
ok:true,
|
||||
total_credits:(.data.total_credits),
|
||||
total_usage:(.data.total_usage),
|
||||
remaining:((.data.total_credits) - (.data.total_usage))
|
||||
}' 2>/dev/null || printf '{"ok":false,"error":"bad_json","detail":"impossibile elaborare OpenRouter"}'
|
||||
}
|
||||
|
||||
DS_JSON="$(ds_block)"
|
||||
OR_JSON="$(or_block)"
|
||||
|
||||
# ok globale = almeno un provider sano.
|
||||
GLOBAL_OK="$(jq -n --argjson d "$DS_JSON" --argjson o "$OR_JSON" '($d.ok or $o.ok)')"
|
||||
|
||||
jq -nc \
|
||||
--argjson ok "$GLOBAL_OK" \
|
||||
--argjson deepseek "$DS_JSON" \
|
||||
--argjson openrouter "$OR_JSON" \
|
||||
--argjson fetched "$NOW" \
|
||||
'{ok:$ok, deepseek:$deepseek, openrouter:$openrouter, fetched_at:$fetched}'
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,120 @@
|
||||
/* Card per un servizio AI: saldo residuo grande + barra + sottocampi. */
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string title: ""
|
||||
property string badge: "" // es. "USD"
|
||||
property color accent: "#00bcd4"
|
||||
property string remainingText: "—" // testo grande del residuo (es. "$1.56")
|
||||
property string subText: "" // riga secondaria
|
||||
property string footText: "" // riga footer (es. "16% · usati $8.44")
|
||||
property real progress: 1.0 // 0..1 per la barra; NaN per nasconderla
|
||||
property bool available: true
|
||||
property bool hasError: false
|
||||
property string errorText: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
Layout.preferredHeight: 54
|
||||
radius: 6
|
||||
color: Qt.rgba(0, 0, 0, 0.26)
|
||||
border.width: 1
|
||||
border.color: root.accent
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 5
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 34
|
||||
Layout.preferredHeight: 14
|
||||
radius: 8
|
||||
color: root.accent
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
text: root.badge
|
||||
color: "#101012"
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
PC3.Label {
|
||||
text: root.title
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
}
|
||||
Rectangle {
|
||||
visible: !root.hasError
|
||||
Layout.preferredWidth: 8
|
||||
Layout.preferredHeight: 8
|
||||
radius: 4
|
||||
color: root.available ? root.accent : Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
PC3.Label {
|
||||
text: root.remainingText
|
||||
color: root.accent
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 6
|
||||
visible: !root.hasError && !isNaN(root.progress)
|
||||
Rectangle {
|
||||
id: track
|
||||
anchors.fill: parent
|
||||
radius: 4
|
||||
color: Qt.rgba(0, 0, 0, 0.55)
|
||||
}
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: track.width * Math.max(0, Math.min(1, root.progress))
|
||||
height: parent.height
|
||||
radius: 4
|
||||
color: root.accent
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
visible: root.hasError
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
color: Kirigami.Theme.negativeTextColor
|
||||
text: root.errorText
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.hasError
|
||||
PC3.Label {
|
||||
text: root.subText
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 0
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
PC3.Label {
|
||||
text: root.footText
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* configGeneral.qml — pagina di configurazione del plasmoide AI Credits Balance.
|
||||
*/
|
||||
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
|
||||
property alias cfg_dsBudget: budgetSpin.value
|
||||
property alias cfg_showDeepSeek: dsCheck.checked
|
||||
property alias cfg_showOpenRouter: orCheck.checked
|
||||
|
||||
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 }
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("Budget di riferimento DeepSeek ($)")
|
||||
font.bold: true
|
||||
}
|
||||
PC3.SpinBox {
|
||||
id: budgetSpin
|
||||
from: 0
|
||||
to: 100000
|
||||
stepSize: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
PC3.Label {
|
||||
text: i18n("Usato per calcolare % e barra di DeepSeek. 0 = nessuna barra (solo valore).")
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
PC3.CheckBox { id: dsCheck; text: i18n("Mostra DeepSeek") }
|
||||
PC3.CheckBox { id: orCheck; text: i18n("Mostra OpenRouter") }
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
/* AI Credits Balance — 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
|
||||
property string lastUpdate: ""
|
||||
property bool loading: false
|
||||
|
||||
property int refreshSecs: Plasmoid.configuration.refreshIntervalSec
|
||||
property int warnThreshold: Plasmoid.configuration.warnThreshold
|
||||
property int critThreshold: Plasmoid.configuration.critThreshold
|
||||
property real dsBudget: Plasmoid.configuration.dsBudget
|
||||
property bool showDeepSeek: Plasmoid.configuration.showDeepSeek
|
||||
property bool showOpenRouter: Plasmoid.configuration.showOpenRouter
|
||||
|
||||
readonly property color dsAccent: "#4f93ff"
|
||||
readonly property color orAccent: "#00bcd4"
|
||||
|
||||
Plasmoid.title: i18n("AI Credits")
|
||||
Plasmoid.icon: "view-statistics"
|
||||
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
|
||||
toolTipMainText: i18n("Credito AI residuo")
|
||||
toolTipSubText: root.toolTipSub()
|
||||
|
||||
// Desktop-first; la vista compatta è solo fallback per pannelli.
|
||||
preferredRepresentation: fullRepresentation
|
||||
switchWidth: 150
|
||||
switchHeight: 150
|
||||
|
||||
Plasma5Support.DataSource {
|
||||
id: executable
|
||||
engine: "executable"
|
||||
connectedSources: []
|
||||
|
||||
onNewData: function(sourceName, data) {
|
||||
root.loading = false
|
||||
executable.disconnectSource(sourceName)
|
||||
if (data["exit code"] !== 0) {
|
||||
// lo script non fallisce quasi mai (gestione interna), ma…
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse((data["stdout"] || "").trim())
|
||||
if (result) {
|
||||
root.data = result
|
||||
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (root.loading) return
|
||||
root.loading = true
|
||||
const path = Qt.resolvedUrl("../scripts/balance.sh").toString().replace(/^file:\/\//, "")
|
||||
executable.connectSource("/bin/bash " + shellQuote(path))
|
||||
}
|
||||
function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" }
|
||||
|
||||
// Colore accent in base alla percentuale residua.
|
||||
function accentFor(pct, normalAccent) {
|
||||
if (isNaN(pct)) return normalAccent
|
||||
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor
|
||||
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
|
||||
return normalAccent
|
||||
}
|
||||
function money(v) {
|
||||
if (typeof v !== "number" || isNaN(v)) return "—"
|
||||
return "$" + v.toFixed(2)
|
||||
}
|
||||
function pctStr(pct) {
|
||||
return isNaN(pct) ? "" : Math.round(pct) + "%"
|
||||
}
|
||||
|
||||
// DeepSeek
|
||||
function dsData() { return root.data && root.data.deepseek ? root.data.deepseek : null }
|
||||
function dsRemainingPct() {
|
||||
const d = dsData()
|
||||
if (!d || !d.ok || root.dsBudget <= 0) return NaN
|
||||
return Math.max(0, (d.total / root.dsBudget) * 100)
|
||||
}
|
||||
function dsProgress() {
|
||||
const d = dsData()
|
||||
if (!d || !d.ok || root.dsBudget <= 0) return NaN
|
||||
return Math.max(0, Math.min(1, d.total / root.dsBudget))
|
||||
}
|
||||
function dsRemainingText() {
|
||||
const d = dsData()
|
||||
if (!d) return "…"
|
||||
if (!d.ok) return "ERR"
|
||||
return money(d.total)
|
||||
}
|
||||
function dsAccentColor() {
|
||||
const d = dsData()
|
||||
if (!d || !d.ok) return Kirigami.Theme.negativeTextColor
|
||||
const p = dsRemainingPct()
|
||||
return isNaN(p) ? root.dsAccent : root.accentFor(p, root.dsAccent)
|
||||
}
|
||||
function dsSub() {
|
||||
const d = dsData()
|
||||
if (!d || !d.ok) return ""
|
||||
return i18n("Granted %1 · Top-up %2", money(d.granted), money(d.topped_up))
|
||||
}
|
||||
function dsFoot() {
|
||||
const d = dsData()
|
||||
if (!d || !d.ok) return ""
|
||||
const p = dsRemainingPct()
|
||||
if (isNaN(p)) return d.available ? i18n("disponibile") : i18n("NON disponibile")
|
||||
return (d.available ? i18n("disponibile · %1", pctStr(p)) : i18n("NON disponibile · %1", pctStr(p)))
|
||||
}
|
||||
function dsError() {
|
||||
const d = dsData()
|
||||
if (!d || d.ok) return ""
|
||||
const map = { "no_api_key": i18n("nessuna chiave DeepSeek"), "fetch_failed": i18n("connessione fallita"), "bad_json": i18n("risposta non valida") }
|
||||
return map[d.error] ? map[d.error] : i18n("errore")
|
||||
}
|
||||
|
||||
// OpenRouter
|
||||
function orData() { return root.data && root.data.openrouter ? root.data.openrouter : null }
|
||||
function orRemaining() {
|
||||
const d = orData()
|
||||
return d && d.ok ? d.remaining : NaN
|
||||
}
|
||||
function orRemainingPct() {
|
||||
const d = orData()
|
||||
if (!d || !d.ok || !d.total_credits) return NaN
|
||||
return Math.max(0, (d.remaining / d.total_credits) * 100)
|
||||
}
|
||||
function orProgress() {
|
||||
const d = orData()
|
||||
if (!d || !d.ok || !d.total_credits) return NaN
|
||||
return Math.max(0, Math.min(1, d.remaining / d.total_credits))
|
||||
}
|
||||
function orRemainingText() {
|
||||
const d = orData()
|
||||
if (!d) return "…"
|
||||
if (!d.ok) return "ERR"
|
||||
return money(d.remaining)
|
||||
}
|
||||
function orAccentColor() {
|
||||
const d = orData()
|
||||
if (!d || !d.ok) return Kirigami.Theme.negativeTextColor
|
||||
return root.accentFor(orRemainingPct(), root.orAccent)
|
||||
}
|
||||
function orSub() {
|
||||
const d = orData()
|
||||
if (!d || !d.ok) return ""
|
||||
return i18n("Usati %1 / %2", money(d.total_usage), money(d.total_credits))
|
||||
}
|
||||
function orFoot() {
|
||||
const d = orData()
|
||||
if (!d || !d.ok) return ""
|
||||
return pctStr(orRemainingPct())
|
||||
}
|
||||
function orError() {
|
||||
const d = orData()
|
||||
if (!d || d.ok) return ""
|
||||
const map = { "no_api_key": i18n("nessuna chiave OpenRouter"), "fetch_failed": i18n("connessione fallita"), "bad_json": i18n("risposta non valida (forse serve una management key)") }
|
||||
return map[d.error] ? map[d.error] : i18n("errore")
|
||||
}
|
||||
|
||||
function toolTipSub() {
|
||||
if (!root.data) return i18n("Aggiornamento…")
|
||||
const d = dsData(), o = orData()
|
||||
const ds = d && d.ok ? money(d.total) : "—"
|
||||
const ors = o && o.ok ? money(o.remaining) : "—"
|
||||
return i18n("DeepSeek %1 · OpenRouter %2", ds, ors)
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
Timer {
|
||||
interval: Math.max(30, root.refreshSecs) * 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
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.data.ok ? root.dsAccent : Kirigami.Theme.disabledTextColor
|
||||
}
|
||||
MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded }
|
||||
}
|
||||
}
|
||||
|
||||
fullRepresentation: Component {
|
||||
Item {
|
||||
implicitWidth: 224
|
||||
implicitHeight: 232
|
||||
Layout.preferredWidth: 224
|
||||
Layout.preferredHeight: 232
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 5
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Kirigami.Icon {
|
||||
source: Plasmoid.icon
|
||||
color: root.dsAccent
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
PC3.Label {
|
||||
text: i18n("CREDITO AI")
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
|
||||
}
|
||||
PC3.Label {
|
||||
text: root.data && root.data.ok ? i18n("RESIDUO") : i18n("OFFLINE")
|
||||
color: root.data && root.data.ok ? root.dsAccent : Kirigami.Theme.negativeTextColor
|
||||
font.bold: true
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 9
|
||||
Layout.preferredHeight: 9
|
||||
radius: 5
|
||||
color: root.data && root.data.ok ? "#7cb342" : Kirigami.Theme.negativeTextColor
|
||||
}
|
||||
PC3.Button {
|
||||
icon.name: "view-refresh"
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
enabled: !root.loading
|
||||
onClicked: root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Qt.rgba(0, 188 / 255, 212 / 255, 0.45)
|
||||
}
|
||||
|
||||
CreditCard {
|
||||
Layout.fillWidth: true
|
||||
visible: root.showDeepSeek
|
||||
title: i18n("DeepSeek")
|
||||
badge: dsData() && dsData().ok ? dsData().currency : "DS"
|
||||
accent: root.dsAccentColor()
|
||||
remainingText: root.dsRemainingText()
|
||||
subText: root.dsSub()
|
||||
footText: root.dsFoot()
|
||||
progress: root.dsProgress()
|
||||
available: dsData() ? dsData().available : false
|
||||
hasError: dsData() ? !dsData().ok : false
|
||||
errorText: root.dsError()
|
||||
}
|
||||
|
||||
CreditCard {
|
||||
Layout.fillWidth: true
|
||||
visible: root.showOpenRouter
|
||||
title: i18n("OpenRouter")
|
||||
badge: "USD"
|
||||
accent: root.orAccentColor()
|
||||
remainingText: root.orRemainingText()
|
||||
subText: root.orSub()
|
||||
footText: root.orFoot()
|
||||
progress: root.orProgress()
|
||||
available: orData() ? orData().ok : false
|
||||
hasError: orData() ? !orData().ok : false
|
||||
errorText: root.orError()
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// Footer sync
|
||||
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.loading ? i18n("aggiornamento…") : ""
|
||||
color: Kirigami.Theme.disabledTextColor
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/Applet",
|
||||
"KPlugin": {
|
||||
"Id": "org.enne2.ai-credits.balance",
|
||||
"Name": "AI Credits Balance",
|
||||
"Name[it]": "Credito AI Residuo",
|
||||
"Description": "Shows the remaining credit for DeepSeek and OpenRouter, reading the API keys from ~/.pi/agent/auth.json (the 'deepseek' and 'openrouter' providers of the pi agent)",
|
||||
"Description[it]": "Mostra il credito residuo di DeepSeek e OpenRouter, leggendo le chiavi API da ~/.pi/agent/auth.json (provider 'deepseek' e 'openrouter' di pi agent)",
|
||||
"Icon": "view-statistics",
|
||||
"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